mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 23:17:15 +00:00
* chore(lint): reduce underscore-dangle exceptions * chore(lint): reduce more underscore exceptions * chore(lint): remove underscore-dangle allow list * fix(lint): repair underscore cleanup regressions * test(lint): track version define suppression
677 lines
24 KiB
TypeScript
677 lines
24 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import {
|
|
clearInternalHooks,
|
|
registerInternalHook,
|
|
type AgentBootstrapHookContext,
|
|
} from "../hooks/internal-hooks.js";
|
|
import { makeTempWorkspace } from "../test-helpers/workspace.js";
|
|
import {
|
|
resetBootstrapWarningCacheForTest,
|
|
FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE,
|
|
hasCompletedBootstrapTurn,
|
|
makeBootstrapWarn,
|
|
resolveBootstrapContextForRun,
|
|
resolveBootstrapFilesForRun,
|
|
resolveContextInjectionMode,
|
|
} from "./bootstrap-files.js";
|
|
import type { WorkspaceBootstrapFile } from "./workspace.js";
|
|
|
|
function registerExtraBootstrapFileHook() {
|
|
registerInternalHook("agent:bootstrap", (event) => {
|
|
const context = event.context as AgentBootstrapHookContext;
|
|
context.bootstrapFiles = [
|
|
...context.bootstrapFiles,
|
|
{
|
|
name: "EXTRA.md",
|
|
path: path.join(context.workspaceDir, "EXTRA.md"),
|
|
content: "extra",
|
|
missing: false,
|
|
} as unknown as WorkspaceBootstrapFile,
|
|
];
|
|
});
|
|
}
|
|
|
|
function registerMalformedBootstrapFileHook() {
|
|
registerInternalHook("agent:bootstrap", (event) => {
|
|
const context = event.context as AgentBootstrapHookContext;
|
|
context.bootstrapFiles = [
|
|
...context.bootstrapFiles,
|
|
{
|
|
name: "EXTRA.md",
|
|
filePath: path.join(context.workspaceDir, "BROKEN.md"),
|
|
content: "broken",
|
|
missing: false,
|
|
} as unknown as WorkspaceBootstrapFile,
|
|
{
|
|
name: "EXTRA.md",
|
|
path: 123,
|
|
content: "broken",
|
|
missing: false,
|
|
} as unknown as WorkspaceBootstrapFile,
|
|
{
|
|
name: "EXTRA.md",
|
|
path: " ",
|
|
content: "broken",
|
|
missing: false,
|
|
} as unknown as WorkspaceBootstrapFile,
|
|
];
|
|
});
|
|
}
|
|
|
|
function registerDuplicateBootstrapFileHook() {
|
|
registerInternalHook("agent:bootstrap", (event) => {
|
|
const context = event.context as AgentBootstrapHookContext;
|
|
context.bootstrapFiles = [
|
|
...context.bootstrapFiles,
|
|
{
|
|
name: "AGENTS.md",
|
|
path: "AGENTS.md",
|
|
content: "duplicate relative hook content",
|
|
missing: false,
|
|
},
|
|
{
|
|
name: "AGENTS.md",
|
|
path: path.join(context.workspaceDir, ".", "AGENTS.md"),
|
|
content: "duplicate absolute hook content",
|
|
missing: false,
|
|
},
|
|
];
|
|
});
|
|
}
|
|
|
|
function registerBootstrapFileHook(relativePath = "BOOTSTRAP.md") {
|
|
registerInternalHook("agent:bootstrap", (event) => {
|
|
const context = event.context as AgentBootstrapHookContext;
|
|
context.bootstrapFiles = [
|
|
...context.bootstrapFiles,
|
|
{
|
|
name: "BOOTSTRAP.md",
|
|
path: path.join(context.workspaceDir, relativePath),
|
|
content: "stale ritual",
|
|
missing: false,
|
|
},
|
|
];
|
|
});
|
|
}
|
|
|
|
async function createHeartbeatAgentsWorkspace() {
|
|
const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
|
|
await fs.writeFile(path.join(workspaceDir, "HEARTBEAT.md"), "check inbox", "utf8");
|
|
await fs.writeFile(path.join(workspaceDir, "AGENTS.md"), "repo rules", "utf8");
|
|
return workspaceDir;
|
|
}
|
|
|
|
function expectHeartbeatExcludedAndAgentsKept(files: WorkspaceBootstrapFile[]) {
|
|
const fileNames = files.map((file) => file.name);
|
|
expect(fileNames).not.toContain("HEARTBEAT.md");
|
|
expect(fileNames).toContain("AGENTS.md");
|
|
}
|
|
|
|
describe("resolveBootstrapFilesForRun", () => {
|
|
beforeEach(() => clearInternalHooks());
|
|
afterEach(() => clearInternalHooks());
|
|
|
|
it("applies bootstrap hook overrides", async () => {
|
|
registerExtraBootstrapFileHook();
|
|
|
|
const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
|
|
const files = await resolveBootstrapFilesForRun({ workspaceDir });
|
|
|
|
const filePaths = files.map((file) => file.path);
|
|
expect(filePaths).toContain(path.join(workspaceDir, "EXTRA.md"));
|
|
});
|
|
|
|
it("drops malformed hook files with missing/invalid paths", async () => {
|
|
registerMalformedBootstrapFileHook();
|
|
|
|
const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
|
|
const warnings: string[] = [];
|
|
const files = await resolveBootstrapFilesForRun({
|
|
workspaceDir,
|
|
warn: (message) => warnings.push(message),
|
|
});
|
|
|
|
expect(files.map((file) => path.relative(workspaceDir, file.path))).toEqual([
|
|
"AGENTS.md",
|
|
"SOUL.md",
|
|
"TOOLS.md",
|
|
"IDENTITY.md",
|
|
"USER.md",
|
|
"HEARTBEAT.md",
|
|
"BOOTSTRAP.md",
|
|
]);
|
|
expect(warnings).toHaveLength(3);
|
|
expect(warnings[0]).toContain('missing or invalid "path" field');
|
|
});
|
|
|
|
it("dedupes hook-injected bootstrap paths relative to the workspace", async () => {
|
|
registerDuplicateBootstrapFileHook();
|
|
|
|
const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
|
|
const agentsPath = path.join(workspaceDir, "AGENTS.md");
|
|
await fs.writeFile(agentsPath, "workspace rules", "utf8");
|
|
|
|
const files = await resolveBootstrapFilesForRun({ workspaceDir });
|
|
const agentsFiles = files.filter((file) => file.path === agentsPath);
|
|
|
|
expect(agentsFiles).toHaveLength(1);
|
|
expect(agentsFiles[0]?.content).toBe("workspace rules");
|
|
|
|
const context = await resolveBootstrapContextForRun({ workspaceDir });
|
|
const agentsContextFiles = context.contextFiles.filter((file) => file.path === agentsPath);
|
|
expect(agentsContextFiles).toHaveLength(1);
|
|
expect(agentsContextFiles[0]?.content).toBe("workspace rules");
|
|
});
|
|
|
|
it("ignores stale workspace BOOTSTRAP.md once setup is completed", async () => {
|
|
const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
|
|
await fs.mkdir(path.join(workspaceDir, ".openclaw"), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(workspaceDir, ".openclaw", "workspace-state.json"),
|
|
`${JSON.stringify({
|
|
version: 1,
|
|
bootstrapSeededAt: "2026-05-16T00:00:00.000Z",
|
|
setupCompletedAt: "2026-05-16T00:00:01.000Z",
|
|
})}\n`,
|
|
"utf8",
|
|
);
|
|
await fs.writeFile(path.join(workspaceDir, "AGENTS.md"), "rules", "utf8");
|
|
await fs.writeFile(path.join(workspaceDir, "BOOTSTRAP.md"), "stale ritual", "utf8");
|
|
|
|
const files = await resolveBootstrapFilesForRun({ workspaceDir });
|
|
|
|
expect(files.map((file) => file.name)).toContain("AGENTS.md");
|
|
expect(files.map((file) => file.name)).not.toContain("BOOTSTRAP.md");
|
|
});
|
|
|
|
it("keeps BOOTSTRAP.md when setup state cannot be read", async () => {
|
|
const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
|
|
await fs.mkdir(path.join(workspaceDir, ".openclaw", "workspace-state.json"), {
|
|
recursive: true,
|
|
});
|
|
await fs.writeFile(path.join(workspaceDir, "AGENTS.md"), "rules", "utf8");
|
|
await fs.writeFile(path.join(workspaceDir, "BOOTSTRAP.md"), "ritual", "utf8");
|
|
|
|
const files = await resolveBootstrapFilesForRun({ workspaceDir });
|
|
|
|
expect(files.map((file) => file.name)).toContain("BOOTSTRAP.md");
|
|
});
|
|
|
|
it("does not let hooks re-add stale root BOOTSTRAP.md after setup is completed", async () => {
|
|
registerBootstrapFileHook();
|
|
const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
|
|
await fs.mkdir(path.join(workspaceDir, ".openclaw"), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(workspaceDir, ".openclaw", "workspace-state.json"),
|
|
`${JSON.stringify({
|
|
version: 1,
|
|
bootstrapSeededAt: "2026-05-16T00:00:00.000Z",
|
|
setupCompletedAt: "2026-05-16T00:00:01.000Z",
|
|
})}\n`,
|
|
"utf8",
|
|
);
|
|
await fs.writeFile(path.join(workspaceDir, "AGENTS.md"), "rules", "utf8");
|
|
await fs.writeFile(path.join(workspaceDir, "BOOTSTRAP.md"), "stale ritual", "utf8");
|
|
|
|
const files = await resolveBootstrapFilesForRun({ workspaceDir });
|
|
|
|
expect(files.map((file) => file.name)).not.toContain("BOOTSTRAP.md");
|
|
});
|
|
|
|
it("ignores stale root BOOTSTRAP.md for home-relative workspace paths", async () => {
|
|
registerBootstrapFileHook();
|
|
const parentDir = await makeTempWorkspace("openclaw-bootstrap-home-");
|
|
const workspaceDir = path.join(parentDir, "workspace");
|
|
await fs.mkdir(path.join(workspaceDir, ".openclaw"), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(workspaceDir, ".openclaw", "workspace-state.json"),
|
|
`${JSON.stringify({
|
|
version: 1,
|
|
bootstrapSeededAt: "2026-05-16T00:00:00.000Z",
|
|
setupCompletedAt: "2026-05-16T00:00:01.000Z",
|
|
})}\n`,
|
|
"utf8",
|
|
);
|
|
await fs.writeFile(path.join(workspaceDir, "AGENTS.md"), "rules", "utf8");
|
|
await fs.writeFile(path.join(workspaceDir, "BOOTSTRAP.md"), "stale ritual", "utf8");
|
|
|
|
const previousOpenClawHome = process.env.OPENCLAW_HOME;
|
|
process.env.OPENCLAW_HOME = parentDir;
|
|
try {
|
|
const files = await resolveBootstrapFilesForRun({ workspaceDir: "~/workspace" });
|
|
|
|
expect(files.map((file) => file.name)).toContain("AGENTS.md");
|
|
expect(files.map((file) => file.name)).not.toContain("BOOTSTRAP.md");
|
|
} finally {
|
|
if (previousOpenClawHome === undefined) {
|
|
delete process.env.OPENCLAW_HOME;
|
|
} else {
|
|
process.env.OPENCLAW_HOME = previousOpenClawHome;
|
|
}
|
|
}
|
|
});
|
|
|
|
it("keeps hook-added nested BOOTSTRAP.md after setup is completed", async () => {
|
|
registerBootstrapFileHook(path.join("packages", "core", "BOOTSTRAP.md"));
|
|
const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
|
|
await fs.mkdir(path.join(workspaceDir, ".openclaw"), { recursive: true });
|
|
await fs.mkdir(path.join(workspaceDir, "packages", "core"), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(workspaceDir, ".openclaw", "workspace-state.json"),
|
|
`${JSON.stringify({
|
|
version: 1,
|
|
bootstrapSeededAt: "2026-05-16T00:00:00.000Z",
|
|
setupCompletedAt: "2026-05-16T00:00:01.000Z",
|
|
})}\n`,
|
|
"utf8",
|
|
);
|
|
await fs.writeFile(path.join(workspaceDir, "AGENTS.md"), "rules", "utf8");
|
|
await fs.writeFile(path.join(workspaceDir, "BOOTSTRAP.md"), "stale ritual", "utf8");
|
|
await fs.writeFile(
|
|
path.join(workspaceDir, "packages", "core", "BOOTSTRAP.md"),
|
|
"package ritual",
|
|
"utf8",
|
|
);
|
|
|
|
const files = await resolveBootstrapFilesForRun({ workspaceDir });
|
|
|
|
expect(files.map((file) => path.relative(workspaceDir, file.path))).toContain(
|
|
path.join("packages", "core", "BOOTSTRAP.md"),
|
|
);
|
|
expect(files.map((file) => file.path)).not.toContain(path.join(workspaceDir, "BOOTSTRAP.md"));
|
|
});
|
|
});
|
|
|
|
describe("resolveBootstrapContextForRun", () => {
|
|
beforeEach(() => clearInternalHooks());
|
|
afterEach(() => clearInternalHooks());
|
|
|
|
it("returns context files for hook-adjusted bootstrap files", async () => {
|
|
registerExtraBootstrapFileHook();
|
|
|
|
const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
|
|
const result = await resolveBootstrapContextForRun({ workspaceDir });
|
|
const extra = result.contextFiles.find(
|
|
(file) => file.path === path.join(workspaceDir, "EXTRA.md"),
|
|
);
|
|
|
|
expect(extra?.content).toBe("extra");
|
|
});
|
|
|
|
it("keeps BOOTSTRAP.md available in shared injected context for non-attempt consumers", async () => {
|
|
const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
|
|
await fs.writeFile(path.join(workspaceDir, "BOOTSTRAP.md"), "ritual", "utf8");
|
|
await fs.writeFile(path.join(workspaceDir, "AGENTS.md"), "rules", "utf8");
|
|
|
|
const result = await resolveBootstrapContextForRun({ workspaceDir });
|
|
|
|
const bootstrapFileNames = result.bootstrapFiles.map((file) => file.name);
|
|
expect(bootstrapFileNames).toContain("BOOTSTRAP.md");
|
|
const contextFileNames = new Set(result.contextFiles.map((file) => path.basename(file.path)));
|
|
expect(contextFileNames.has("BOOTSTRAP.md")).toBe(true);
|
|
expect(contextFileNames.has("AGENTS.md")).toBe(true);
|
|
});
|
|
|
|
it("uses heartbeat-only bootstrap files in lightweight heartbeat mode", async () => {
|
|
const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
|
|
await fs.writeFile(path.join(workspaceDir, "HEARTBEAT.md"), "check inbox", "utf8");
|
|
await fs.writeFile(path.join(workspaceDir, "SOUL.md"), "persona", "utf8");
|
|
|
|
const files = await resolveBootstrapFilesForRun({
|
|
workspaceDir,
|
|
contextMode: "lightweight",
|
|
runKind: "heartbeat",
|
|
});
|
|
|
|
expect(files.map((file) => file.name)).toStrictEqual(["HEARTBEAT.md"]);
|
|
expect(files[0]?.content).toBe("check inbox");
|
|
});
|
|
|
|
it("keeps bootstrap context empty in lightweight cron mode", async () => {
|
|
const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
|
|
await fs.writeFile(path.join(workspaceDir, "HEARTBEAT.md"), "check inbox", "utf8");
|
|
|
|
const files = await resolveBootstrapFilesForRun({
|
|
workspaceDir,
|
|
contextMode: "lightweight",
|
|
runKind: "cron",
|
|
});
|
|
|
|
expect(files).toStrictEqual([]);
|
|
});
|
|
|
|
it("drops HEARTBEAT.md for non-heartbeat runs when the heartbeat prompt section is disabled", async () => {
|
|
const workspaceDir = await createHeartbeatAgentsWorkspace();
|
|
|
|
const files = await resolveBootstrapFilesForRun({
|
|
workspaceDir,
|
|
config: {
|
|
agents: {
|
|
defaults: {
|
|
heartbeat: {
|
|
includeSystemPromptSection: false,
|
|
},
|
|
},
|
|
list: [{ id: "main" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
expectHeartbeatExcludedAndAgentsKept(files);
|
|
});
|
|
|
|
it("drops HEARTBEAT.md for non-heartbeat runs when the heartbeat cadence is disabled", async () => {
|
|
const workspaceDir = await createHeartbeatAgentsWorkspace();
|
|
|
|
const files = await resolveBootstrapFilesForRun({
|
|
workspaceDir,
|
|
config: {
|
|
agents: {
|
|
defaults: {
|
|
heartbeat: {
|
|
every: "0m",
|
|
},
|
|
},
|
|
list: [{ id: "main" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
expectHeartbeatExcludedAndAgentsKept(files);
|
|
});
|
|
|
|
it("keeps HEARTBEAT.md for actual heartbeat runs even when the prompt section is disabled", async () => {
|
|
const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
|
|
await fs.writeFile(path.join(workspaceDir, "HEARTBEAT.md"), "check inbox", "utf8");
|
|
|
|
const files = await resolveBootstrapFilesForRun({
|
|
workspaceDir,
|
|
runKind: "heartbeat",
|
|
config: {
|
|
agents: {
|
|
defaults: {
|
|
heartbeat: {
|
|
includeSystemPromptSection: false,
|
|
},
|
|
},
|
|
list: [{ id: "main" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
const fileNames = files.map((file) => file.name);
|
|
expect(fileNames).toContain("HEARTBEAT.md");
|
|
});
|
|
});
|
|
|
|
describe("hasCompletedBootstrapTurn", () => {
|
|
let tmpDir: string;
|
|
|
|
beforeEach(async () => {
|
|
tmpDir = await fs.mkdtemp(path.join(await fs.realpath("/tmp"), "openclaw-bootstrap-turn-"));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("returns false when session file does not exist", async () => {
|
|
expect(await hasCompletedBootstrapTurn(path.join(tmpDir, "missing.jsonl"))).toBe(false);
|
|
});
|
|
|
|
it("returns false for empty session files", async () => {
|
|
const sessionFile = path.join(tmpDir, "empty.jsonl");
|
|
await fs.writeFile(sessionFile, "", "utf8");
|
|
expect(await hasCompletedBootstrapTurn(sessionFile)).toBe(false);
|
|
});
|
|
|
|
it("returns false for header-only session files", async () => {
|
|
const sessionFile = path.join(tmpDir, "header-only.jsonl");
|
|
await fs.writeFile(sessionFile, `${JSON.stringify({ type: "session", id: "s1" })}\n`, "utf8");
|
|
expect(await hasCompletedBootstrapTurn(sessionFile)).toBe(false);
|
|
});
|
|
|
|
it("returns false when no assistant turn has been flushed yet", async () => {
|
|
const sessionFile = path.join(tmpDir, "user-only.jsonl");
|
|
await fs.writeFile(
|
|
sessionFile,
|
|
[
|
|
JSON.stringify({ type: "session", id: "s1" }),
|
|
JSON.stringify({ type: "message", message: { role: "user", content: "hello" } }),
|
|
].join("\n") + "\n",
|
|
"utf8",
|
|
);
|
|
expect(await hasCompletedBootstrapTurn(sessionFile)).toBe(false);
|
|
});
|
|
|
|
it("returns false for assistant turns without a recorded full bootstrap marker", async () => {
|
|
const sessionFile = path.join(tmpDir, "assistant-no-marker.jsonl");
|
|
await fs.writeFile(
|
|
sessionFile,
|
|
[
|
|
JSON.stringify({ type: "session", id: "s1" }),
|
|
JSON.stringify({ type: "message", message: { role: "user", content: "hello" } }),
|
|
JSON.stringify({ type: "message", message: { role: "assistant", content: "hi" } }),
|
|
].join("\n") + "\n",
|
|
"utf8",
|
|
);
|
|
expect(await hasCompletedBootstrapTurn(sessionFile)).toBe(false);
|
|
});
|
|
|
|
it("returns true when a full bootstrap completion marker exists", async () => {
|
|
const sessionFile = path.join(tmpDir, "full-bootstrap.jsonl");
|
|
await fs.writeFile(
|
|
sessionFile,
|
|
[
|
|
JSON.stringify({ type: "message", message: { role: "assistant", content: "hi" } }),
|
|
JSON.stringify({
|
|
type: "custom",
|
|
customType: FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE,
|
|
data: { timestamp: 1 },
|
|
}),
|
|
].join("\n") + "\n",
|
|
"utf8",
|
|
);
|
|
expect(await hasCompletedBootstrapTurn(sessionFile)).toBe(true);
|
|
});
|
|
|
|
it("returns false when compaction happened after the last assistant turn", async () => {
|
|
const sessionFile = path.join(tmpDir, "post-compaction.jsonl");
|
|
await fs.writeFile(
|
|
sessionFile,
|
|
[
|
|
JSON.stringify({
|
|
type: "custom",
|
|
customType: FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE,
|
|
data: { timestamp: 1 },
|
|
}),
|
|
JSON.stringify({ type: "compaction", summary: "trimmed" }),
|
|
].join("\n") + "\n",
|
|
"utf8",
|
|
);
|
|
expect(await hasCompletedBootstrapTurn(sessionFile)).toBe(false);
|
|
});
|
|
|
|
it("returns true when a later full bootstrap marker happens after compaction", async () => {
|
|
const sessionFile = path.join(tmpDir, "assistant-after-compaction.jsonl");
|
|
await fs.writeFile(
|
|
sessionFile,
|
|
[
|
|
JSON.stringify({
|
|
type: "custom",
|
|
customType: FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE,
|
|
data: { timestamp: 1 },
|
|
}),
|
|
JSON.stringify({ type: "compaction", summary: "trimmed" }),
|
|
JSON.stringify({ type: "message", message: { role: "user", content: "new ask" } }),
|
|
JSON.stringify({ type: "message", message: { role: "assistant", content: "new reply" } }),
|
|
JSON.stringify({
|
|
type: "custom",
|
|
customType: FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE,
|
|
data: { timestamp: 2 },
|
|
}),
|
|
].join("\n") + "\n",
|
|
"utf8",
|
|
);
|
|
expect(await hasCompletedBootstrapTurn(sessionFile)).toBe(true);
|
|
});
|
|
|
|
it("ignores malformed JSON lines", async () => {
|
|
const sessionFile = path.join(tmpDir, "malformed.jsonl");
|
|
await fs.writeFile(
|
|
sessionFile,
|
|
[
|
|
"{broken",
|
|
JSON.stringify({
|
|
type: "custom",
|
|
customType: FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE,
|
|
data: { timestamp: 1 },
|
|
}),
|
|
].join("\n") + "\n",
|
|
"utf8",
|
|
);
|
|
expect(await hasCompletedBootstrapTurn(sessionFile)).toBe(true);
|
|
});
|
|
|
|
it("finds a recent full bootstrap marker even when the scan starts mid-file", async () => {
|
|
const sessionFile = path.join(tmpDir, "large-prefix.jsonl");
|
|
const hugePrefix = "x".repeat(300 * 1024);
|
|
await fs.writeFile(
|
|
sessionFile,
|
|
[
|
|
JSON.stringify({ type: "message", message: { role: "user", content: hugePrefix } }),
|
|
JSON.stringify({
|
|
type: "custom",
|
|
customType: FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE,
|
|
data: { timestamp: 1 },
|
|
}),
|
|
].join("\n") + "\n",
|
|
"utf8",
|
|
);
|
|
expect(await hasCompletedBootstrapTurn(sessionFile)).toBe(true);
|
|
});
|
|
|
|
it("returns false for symbolic links", async () => {
|
|
const realFile = path.join(tmpDir, "real.jsonl");
|
|
const linkFile = path.join(tmpDir, "link.jsonl");
|
|
await fs.writeFile(
|
|
realFile,
|
|
`${JSON.stringify({ type: "custom", customType: FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE, data: { timestamp: 1 } })}\n`,
|
|
"utf8",
|
|
);
|
|
await fs.symlink(realFile, linkFile);
|
|
expect(await hasCompletedBootstrapTurn(linkFile)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("makeBootstrapWarn", () => {
|
|
afterEach(() => {
|
|
resetBootstrapWarningCacheForTest();
|
|
});
|
|
|
|
it("deduplicates repeated warnings for the same session and message", () => {
|
|
const warnings: string[] = [];
|
|
const warn = makeBootstrapWarn({
|
|
sessionLabel: "agent:main:test-session",
|
|
warn: (message) => warnings.push(message),
|
|
});
|
|
|
|
warn?.("workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating");
|
|
warn?.("workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating");
|
|
|
|
expect(warnings).toEqual([
|
|
"workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating (sessionKey=agent:main:test-session)",
|
|
]);
|
|
});
|
|
|
|
it("keeps warnings distinct across sessions", () => {
|
|
const warnings: string[] = [];
|
|
const first = makeBootstrapWarn({
|
|
sessionLabel: "agent:main:first-session",
|
|
warn: (message) => warnings.push(message),
|
|
});
|
|
const second = makeBootstrapWarn({
|
|
sessionLabel: "agent:main:second-session",
|
|
warn: (message) => warnings.push(message),
|
|
});
|
|
|
|
first?.("workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating");
|
|
second?.("workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating");
|
|
|
|
expect(warnings).toEqual([
|
|
"workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating (sessionKey=agent:main:first-session)",
|
|
"workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating (sessionKey=agent:main:second-session)",
|
|
]);
|
|
});
|
|
|
|
it("keeps warnings distinct across workspaces with the same session", () => {
|
|
const warnings: string[] = [];
|
|
const first = makeBootstrapWarn({
|
|
sessionLabel: "agent:main:shared-session",
|
|
workspaceDir: "/tmp/workspace-a",
|
|
warn: (message) => warnings.push(message),
|
|
});
|
|
const second = makeBootstrapWarn({
|
|
sessionLabel: "agent:main:shared-session",
|
|
workspaceDir: "/tmp/workspace-b",
|
|
warn: (message) => warnings.push(message),
|
|
});
|
|
|
|
first?.("workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating");
|
|
second?.("workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating");
|
|
|
|
expect(warnings).toEqual([
|
|
"workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating (sessionKey=agent:main:shared-session)",
|
|
"workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating (sessionKey=agent:main:shared-session)",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("resolveContextInjectionMode", () => {
|
|
it("defaults to always when config is missing", () => {
|
|
expect(resolveContextInjectionMode(undefined)).toBe("always");
|
|
});
|
|
|
|
it("defaults to always when the setting is omitted", () => {
|
|
expect(resolveContextInjectionMode({ agents: { defaults: {} } } as never)).toBe("always");
|
|
});
|
|
|
|
it("returns the configured continuation-skip mode", () => {
|
|
expect(
|
|
resolveContextInjectionMode({
|
|
agents: { defaults: { contextInjection: "continuation-skip" } },
|
|
} as never),
|
|
).toBe("continuation-skip");
|
|
});
|
|
|
|
it("uses per-agent contextInjection before defaults", () => {
|
|
expect(
|
|
resolveContextInjectionMode(
|
|
{
|
|
agents: {
|
|
defaults: { contextInjection: "continuation-skip" },
|
|
list: [{ id: "strict", contextInjection: "always" }],
|
|
},
|
|
} as never,
|
|
"strict",
|
|
),
|
|
).toBe("always");
|
|
});
|
|
|
|
it("falls back to defaults when the agent has no contextInjection override", () => {
|
|
expect(
|
|
resolveContextInjectionMode(
|
|
{
|
|
agents: {
|
|
defaults: { contextInjection: "never" },
|
|
list: [{ id: "worker" }],
|
|
},
|
|
} as never,
|
|
"worker",
|
|
),
|
|
).toBe("never");
|
|
});
|
|
});
|