test: dedupe memory and context suites

This commit is contained in:
Peter Steinberger
2026-04-25 14:06:17 +01:00
parent 2413c0f5a5
commit cd392b947c
7 changed files with 344 additions and 383 deletions

View File

@@ -265,26 +265,31 @@ describe("buildFileEntry", () => {
expect(built?.structuredInputBytes).toBeGreaterThan(0);
});
it("skips lazy multimodal indexing when the file grows after discovery", async () => {
const tmpDir = getTmpDir();
const target = path.join(tmpDir, "diagram.png");
await fs.writeFile(target, Buffer.from("png"));
it("skips lazy multimodal indexing when file state changes after discovery", async () => {
for (const testCase of [
{
name: "grows",
mutate: async (target: string, entrySize: number) => {
await fs.writeFile(target, Buffer.alloc(entrySize + 32, 1));
},
},
{
name: "bytes change",
mutate: async (target: string) => {
await fs.writeFile(target, Buffer.from("gif"));
},
},
] as const) {
const tmpDir = getTmpDir();
const target = path.join(tmpDir, `${testCase.name}.png`);
await fs.writeFile(target, Buffer.from("png"));
const entry = await buildFileEntry(target, tmpDir, multimodal);
await fs.writeFile(target, Buffer.alloc(entry!.size + 32, 1));
const entry = await buildFileEntry(target, tmpDir, multimodal);
expect(entry, testCase.name).not.toBeNull();
await testCase.mutate(target, entry!.size);
await expect(buildMultimodalChunkForIndexing(entry!)).resolves.toBeNull();
});
it("skips lazy multimodal indexing when file bytes change after discovery", async () => {
const tmpDir = getTmpDir();
const target = path.join(tmpDir, "diagram.png");
await fs.writeFile(target, Buffer.from("png"));
const entry = await buildFileEntry(target, tmpDir, multimodal);
await fs.writeFile(target, Buffer.from("gif"));
await expect(buildMultimodalChunkForIndexing(entry!)).resolves.toBeNull();
await expect(buildMultimodalChunkForIndexing(entry!), testCase.name).resolves.toBeNull();
}
});
});

View File

@@ -2,7 +2,7 @@ import { EventEmitter } from "node:events";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const spawnMock = vi.hoisted(() => vi.fn());
@@ -24,25 +24,36 @@ function createMockChild() {
return child;
}
let fixtureRoot = "";
let tempDir = "";
let platformSpy: { mockRestore(): void } | null = null;
let fixtureId = 0;
const originalPath = process.env.PATH;
const originalPathExt = process.env.PATHEXT;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-qmd-win-spawn-"));
beforeAll(async () => {
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-qmd-win-spawn-"));
platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
});
afterEach(async () => {
afterAll(async () => {
platformSpy?.mockRestore();
platformSpy = null;
if (fixtureRoot) {
await fs.rm(fixtureRoot, { recursive: true, force: true });
}
});
beforeEach(async () => {
tempDir = path.join(fixtureRoot, `case-${fixtureId++}`);
await fs.mkdir(tempDir, { recursive: true });
});
afterEach(() => {
process.env.PATH = originalPath;
process.env.PATHEXT = originalPathExt;
spawnMock.mockReset();
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
tempDir = "";
}
tempDir = "";
});
describe("resolveCliSpawnInvocation", () => {

View File

@@ -1,25 +1,35 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { buildSessionEntry, listSessionFilesForAgent } from "./session-files.js";
let fixtureRoot: string;
let tmpDir: string;
let originalStateDir: string | undefined;
let fixtureId = 0;
beforeAll(async () => {
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "session-entry-test-"));
});
afterAll(async () => {
await fs.rm(fixtureRoot, { recursive: true, force: true });
});
beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "session-entry-test-"));
tmpDir = path.join(fixtureRoot, `case-${fixtureId++}`);
await fs.mkdir(tmpDir, { recursive: true });
originalStateDir = process.env.OPENCLAW_STATE_DIR;
process.env.OPENCLAW_STATE_DIR = tmpDir;
});
afterEach(async () => {
afterEach(() => {
if (originalStateDir === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = originalStateDir;
}
await fs.rm(tmpDir, { recursive: true, force: true });
});
describe("listSessionFilesForAgent", () => {

View File

@@ -234,7 +234,7 @@ describe("native hook relay registry", () => {
runId: "run-1",
});
for (const event of ["pre_tool_use", "post_tool_use", "permission_request"] as const) {
for (const event of ["pre_tool_use", "post_tool_use"] as const) {
await expect(
invokeNativeHookRelay({
provider: "codex",

View File

@@ -63,6 +63,12 @@ function makeMockMessage(role: "user" | "assistant" = "user", text = "hello"): A
return { role, content: text, timestamp: Date.now() } as AgentMessage;
}
let uniqueEngineIdCounter = 0;
function uniqueEngineId(prefix: string): string {
uniqueEngineIdCounter += 1;
return `${prefix}-${uniqueEngineIdCounter}`;
}
function registerPromptTrackingEngine(engineId: string) {
const calls: Array<Record<string, unknown>> = [];
registerContextEngine(engineId, () => ({
@@ -701,13 +707,84 @@ describe("Invalid engine fallback", () => {
vi.restoreAllMocks();
});
it("falls back to default engine when requested engine is not registered", async () => {
const engine = await resolveContextEngine(configWithSlot("does-not-exist"));
expect(engine.info.id).toBe("legacy");
expect(console.error).toHaveBeenCalledWith(expect.stringContaining("does-not-exist"));
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining("falling back to default engine"),
);
it("falls back to default engine for missing or invalid requested engines", async () => {
const cases = [
{
name: "missing registration",
engineId: uniqueEngineId("does-not-exist"),
register: () => undefined,
expectedError: "does-not-exist",
},
{
name: "factory throws",
engineId: uniqueEngineId("factory-throw"),
register: (engineId: string) => {
registerContextEngine(engineId, () => {
throw new Error("plugin version mismatch");
});
},
expectedError: "plugin version mismatch",
},
{
name: "missing info metadata",
engineId: uniqueEngineId("invalid-info"),
register: (engineId: string) => {
registerContextEngine(
engineId,
() =>
({
async ingest() {
return { ingested: false };
},
async assemble({ messages }: { messages: AgentMessage[] }) {
return { messages, estimatedTokens: 0 };
},
async compact() {
return { ok: true, compacted: false };
},
}) as unknown as ContextEngine,
);
},
expectedError: "missing info",
},
{
name: "missing lifecycle methods",
engineId: uniqueEngineId("invalid-methods"),
register: (engineId: string) => {
registerContextEngine(
engineId,
() =>
({
info: { id: engineId, name: "Broken Engine" },
async ingest() {
return { ingested: false };
},
}) as unknown as ContextEngine,
);
},
expectedError: "missing assemble(), missing compact()",
},
{
name: "contract validation throws",
engineId: uniqueEngineId("validation-throw"),
register: (engineId: string) => {
registerContextEngine(engineId, () => 42n as unknown as ContextEngine);
},
expectedError: "contract validation threw",
},
] as const;
for (const testCase of cases) {
vi.mocked(console.error).mockClear();
testCase.register(testCase.engineId);
const engine = await resolveContextEngine(configWithSlot(testCase.engineId));
expect(engine.info.id, testCase.name).toBe("legacy");
expect(console.error, testCase.name).toHaveBeenCalledWith(
expect.stringContaining(testCase.expectedError),
);
}
});
it("throws when the default engine itself is not registered", async () => {
@@ -759,43 +836,6 @@ describe("Invalid engine fallback", () => {
);
});
it("falls back to default engine when factory throws", async () => {
const engineId = `factory-throw-${Date.now().toString(36)}`;
registerContextEngine(engineId, () => {
throw new Error("plugin version mismatch");
});
const engine = await resolveContextEngine(configWithSlot(engineId));
expect(engine.info.id).toBe("legacy");
expect(console.error).toHaveBeenCalledWith(expect.stringContaining("plugin version mismatch"));
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining("falling back to default engine"),
);
});
it("falls back to default engine when resolved engine omits info metadata", async () => {
const engineId = `invalid-info-${Date.now().toString(36)}`;
registerContextEngine(
engineId,
() =>
({
async ingest() {
return { ingested: false };
},
async assemble({ messages }: { messages: AgentMessage[] }) {
return { messages, estimatedTokens: 0 };
},
async compact() {
return { ok: true, compacted: false };
},
}) as unknown as ContextEngine,
);
const engine = await resolveContextEngine(configWithSlot(engineId));
expect(engine.info.id).toBe("legacy");
expect(console.error).toHaveBeenCalledWith(expect.stringContaining("missing info"));
});
it("accepts resolved engines whose info.id differs from the registered slot id (#66601)", async () => {
// Regression for openclaw/openclaw#66601: third-party plugins like
// lossless-claw register under an external slot id ("lossless-claw") but
@@ -831,40 +871,6 @@ describe("Invalid engine fallback", () => {
});
expect(result.estimatedTokens).toBe(0);
});
it("falls back to default engine when resolved engine omits lifecycle methods", async () => {
const engineId = `invalid-methods-${Date.now().toString(36)}`;
registerContextEngine(
engineId,
() =>
({
info: { id: engineId, name: "Broken Engine" },
async ingest() {
return { ingested: false };
},
}) as unknown as ContextEngine,
);
const engine = await resolveContextEngine(configWithSlot(engineId));
expect(engine.info.id).toBe("legacy");
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining("missing assemble(), missing compact()"),
);
});
it("falls back to default engine when contract validation itself throws", async () => {
const engineId = `validation-throw-${Date.now().toString(36)}`;
// BigInt cannot be JSON.stringify'd — triggers a throw inside
// describeResolvedContextEngineContractError when the factory returns
// a non-object value that passes the typeof !== "object" branch.
registerContextEngine(engineId, () => 42n as unknown as ContextEngine);
const engine = await resolveContextEngine(configWithSlot(engineId));
expect(engine.info.id).toBe("legacy");
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining("contract validation threw"),
);
});
});
// ═══════════════════════════════════════════════════════════════════════════
@@ -913,54 +919,47 @@ describe("LegacyContextEngine parity", () => {
// ═══════════════════════════════════════════════════════════════════════════
describe("assemble() prompt forwarding", () => {
it("forwards prompt to the underlying engine", async () => {
const engineId = `prompt-fwd-${Date.now().toString(36)}`;
const calls = registerPromptTrackingEngine(engineId);
it("forwards prompt only when callers provide one", async () => {
const cases = [
{
name: "provided",
params: { prompt: "hello" },
expectedPrompt: "hello",
},
{
name: "omitted",
params: {},
expectedPrompt: null,
},
{
name: "conditional spread undefined",
params: (() => {
const callerPrompt: string | undefined = undefined;
return callerPrompt !== undefined ? { prompt: callerPrompt } : {};
})(),
expectedPrompt: null,
},
] as const;
const engine = await resolveContextEngine(configWithSlot(engineId));
await engine.assemble({
sessionId: "s1",
messages: [makeMockMessage("user", "hello")],
prompt: "hello",
});
for (const testCase of cases) {
const engineId = uniqueEngineId(`prompt-${testCase.name.replace(/\s+/g, "-")}`);
const calls = registerPromptTrackingEngine(engineId);
expect(calls).toHaveLength(1);
expect(calls[0]).toHaveProperty("prompt", "hello");
});
const engine = await resolveContextEngine(configWithSlot(engineId));
await engine.assemble({
sessionId: "s1",
messages: [makeMockMessage("user", "hello")],
...testCase.params,
});
it("omits prompt when not provided", async () => {
const engineId = `prompt-omit-${Date.now().toString(36)}`;
const calls = registerPromptTrackingEngine(engineId);
const engine = await resolveContextEngine(configWithSlot(engineId));
await engine.assemble({
sessionId: "s1",
messages: [makeMockMessage("user", "hello")],
});
expect(calls).toHaveLength(1);
expect(calls[0]).not.toHaveProperty("prompt");
});
it("does not leak prompt key when caller spreads undefined", async () => {
// Guards against the pattern `{ prompt: params.prompt }` when params.prompt
// is undefined — JavaScript keeps the key present with value undefined,
// which breaks engines that guard with `'prompt' in params`.
const engineId = `prompt-undef-${Date.now().toString(36)}`;
const calls = registerPromptTrackingEngine(engineId);
const engine = await resolveContextEngine(configWithSlot(engineId));
// Simulate the attempt.ts call-site pattern: conditional spread
const callerPrompt: string | undefined = undefined;
await engine.assemble({
sessionId: "s1",
messages: [makeMockMessage("user", "hello")],
...(callerPrompt !== undefined ? { prompt: callerPrompt } : {}),
});
expect(calls).toHaveLength(1);
expect(calls[0]).not.toHaveProperty("prompt");
expect(Object.keys(calls[0] as object)).not.toContain("prompt");
expect(calls, testCase.name).toHaveLength(1);
if (testCase.expectedPrompt === null) {
expect(calls[0], testCase.name).not.toHaveProperty("prompt");
expect(Object.keys(calls[0] as object), testCase.name).not.toContain("prompt");
} else {
expect(calls[0], testCase.name).toHaveProperty("prompt", testCase.expectedPrompt);
}
}
});
it("retries strict legacy assemble without sessionKey and prompt", async () => {

View File

@@ -152,32 +152,28 @@ describe("listMemoryFiles", () => {
}
});
it("skips root-memory repair backups from extra workspace paths", async () => {
const tmpDir = getTmpDir();
await fs.writeFile(path.join(tmpDir, "MEMORY.md"), "# Default memory");
const repairDir = path.join(tmpDir, ".openclaw-repair", "root-memory", "2026-04-23");
await fs.mkdir(repairDir, { recursive: true });
await fs.writeFile(path.join(repairDir, "memory.md"), "# Archived legacy memory");
it("skips root-memory repair backups from workspace and explicit extra paths", async () => {
for (const testCase of [
{
name: "workspace extra path",
extraPaths: (tmpDir: string) => [tmpDir],
},
{
name: "explicit repair root",
extraPaths: (tmpDir: string) => [path.join(tmpDir, ".openclaw-repair", "root-memory")],
},
] as const) {
const tmpDir = getTmpDir();
await fs.writeFile(path.join(tmpDir, "MEMORY.md"), "# Default memory");
const repairDir = path.join(tmpDir, ".openclaw-repair", "root-memory", "2026-04-23");
await fs.mkdir(repairDir, { recursive: true });
await fs.writeFile(path.join(repairDir, "memory.md"), "# Archived legacy memory");
const files = await listMemoryFiles(tmpDir, [tmpDir]);
const files = await listMemoryFiles(tmpDir, testCase.extraPaths(tmpDir));
expect(files).toHaveLength(1);
expect(files[0]).toBe(path.join(tmpDir, "MEMORY.md"));
});
it("skips explicit root-memory repair directories from extra paths", async () => {
const tmpDir = getTmpDir();
await fs.writeFile(path.join(tmpDir, "MEMORY.md"), "# Default memory");
const repairDir = path.join(tmpDir, ".openclaw-repair", "root-memory", "2026-04-23");
await fs.mkdir(repairDir, { recursive: true });
await fs.writeFile(path.join(repairDir, "memory.md"), "# Archived legacy memory");
const files = await listMemoryFiles(tmpDir, [
path.join(tmpDir, ".openclaw-repair", "root-memory"),
]);
expect(files).toHaveLength(1);
expect(files[0]).toBe(path.join(tmpDir, "MEMORY.md"));
expect(files, testCase.name).toHaveLength(1);
expect(files[0], testCase.name).toBe(path.join(tmpDir, "MEMORY.md"));
}
});
it("handles relative paths in additional paths", async () => {
@@ -324,37 +320,37 @@ describe("buildFileEntry", () => {
expect(built?.structuredInputBytes).toBeGreaterThan(0);
});
it("skips lazy multimodal indexing when the file grows after discovery", async () => {
const tmpDir = getTmpDir();
const target = path.join(tmpDir, "diagram.png");
await fs.writeFile(target, Buffer.from("png"));
it("skips lazy multimodal indexing when file state changes after discovery", async () => {
for (const testCase of [
{
name: "grows",
mutate: async (target: string, entrySize: number) => {
await fs.writeFile(target, Buffer.alloc(entrySize + 32, 1));
},
},
{
name: "bytes change",
mutate: async (target: string) => {
await fs.writeFile(target, Buffer.from("gif"));
},
},
{
name: "disappears",
mutate: async (target: string) => {
await fs.rm(target);
},
},
] as const) {
const tmpDir = getTmpDir();
const target = path.join(tmpDir, `${testCase.name}.png`);
await fs.writeFile(target, Buffer.from("png"));
const entry = await buildFileEntry(target, tmpDir, multimodal);
await fs.writeFile(target, Buffer.alloc(entry!.size + 32, 1));
const entry = await buildFileEntry(target, tmpDir, multimodal);
expect(entry, testCase.name).not.toBeNull();
await testCase.mutate(target, entry!.size);
await expect(buildMultimodalChunkForIndexing(entry!)).resolves.toBeNull();
});
it("skips lazy multimodal indexing when file bytes change after discovery", async () => {
const tmpDir = getTmpDir();
const target = path.join(tmpDir, "diagram.png");
await fs.writeFile(target, Buffer.from("png"));
const entry = await buildFileEntry(target, tmpDir, multimodal);
await fs.writeFile(target, Buffer.from("gif"));
await expect(buildMultimodalChunkForIndexing(entry!)).resolves.toBeNull();
});
it("skips lazy multimodal indexing when the file disappears before loading bytes", async () => {
const tmpDir = getTmpDir();
const target = path.join(tmpDir, "diagram.png");
await fs.writeFile(target, Buffer.from("png"));
const entry = await buildFileEntry(target, tmpDir, multimodal);
await fs.rm(target);
await expect(buildMultimodalChunkForIndexing(entry!)).resolves.toBeNull();
await expect(buildMultimodalChunkForIndexing(entry!), testCase.name).resolves.toBeNull();
}
});
});

View File

@@ -47,6 +47,12 @@ function expectNoUnpairedSurrogates(value: string): void {
}
}
async function writeSessionJsonl(fileName: string, records: readonly unknown[]): Promise<string> {
const filePath = path.join(tmpDir, fileName);
await fs.writeFile(filePath, records.map((record) => JSON.stringify(record)).join("\n"));
return filePath;
}
describe("listSessionFilesForAgent", () => {
it("includes reset and deleted transcripts in session file listing", async () => {
const sessionsDir = path.join(tmpDir, "agents", "main", "sessions");
@@ -483,142 +489,118 @@ describe("buildSessionEntry", () => {
expect(entry?.lineMap).toEqual([1, 2]);
});
it("drops generated system wrapper user messages but keeps the assistant reply", async () => {
// Cross-message coupling (drop-next-assistant-when-prior-user-matched) was
// removed because user-typed text can match the same patterns; see
// PR #70737 review (aisle-research-bot). Real assistant content stays in
// the corpus regardless of what the prior user message looked like.
const jsonlLines = [
JSON.stringify({
type: "message",
message: {
role: "user",
content:
"System (untrusted): [2026-04-15 14:45:20 PDT] Exec completed (quiet-fo, code 0) :: Converted: 1",
},
}),
JSON.stringify({
type: "message",
message: {
role: "assistant",
content: "Handled internally.",
},
}),
JSON.stringify({
type: "message",
message: {
role: "user",
content: "What changed in the sync?",
},
}),
JSON.stringify({
type: "message",
message: {
role: "assistant",
content: "One new session was converted.",
},
}),
];
const filePath = path.join(tmpDir, "system-wrapper-session.jsonl");
await fs.writeFile(filePath, jsonlLines.join("\n"));
it("drops generated runtime chatter while preserving real follow-up content", async () => {
const cases = [
{
name: "system wrapper",
fileName: "system-wrapper-session.jsonl",
records: [
{
type: "message",
message: {
role: "user",
content:
"System (untrusted): [2026-04-15 14:45:20 PDT] Exec completed (quiet-fo, code 0) :: Converted: 1",
},
},
{ type: "message", message: { role: "assistant", content: "Handled internally." } },
{ type: "message", message: { role: "user", content: "What changed in the sync?" } },
{
type: "message",
message: { role: "assistant", content: "One new session was converted." },
},
],
content: [
"Assistant: Handled internally.",
"User: What changed in the sync?",
"Assistant: One new session was converted.",
].join("\n"),
lineMap: [2, 3, 4],
},
{
name: "cron prompt",
fileName: "cron-prompt-session.jsonl",
records: [
{
type: "message",
message: { role: "user", content: "[cron:job-1 Example] Run the nightly sync" },
},
{
type: "message",
message: { role: "assistant", content: "Running the nightly sync now." },
},
{
type: "message",
message: { role: "user", content: "Did the nightly sync actually change anything?" },
},
{
type: "message",
message: { role: "assistant", content: "No, everything was already current." },
},
],
content: [
"Assistant: Running the nightly sync now.",
"User: Did the nightly sync actually change anything?",
"Assistant: No, everything was already current.",
].join("\n"),
lineMap: [2, 3, 4],
},
{
name: "heartbeat ack",
fileName: "heartbeat-session.jsonl",
records: [
{
type: "message",
message: {
role: "user",
content:
"Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.",
},
},
{ type: "message", message: { role: "assistant", content: "HEARTBEAT_OK" } },
{
type: "message",
message: { role: "user", content: "Summarize what changed in the inbox today." },
},
],
content: "User: Summarize what changed in the inbox today.",
lineMap: [3],
},
{
name: "internal runtime context",
fileName: "internal-context-session.jsonl",
records: [
{
type: "message",
message: {
role: "user",
content: [
"<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>",
"OpenClaw runtime context (internal):",
"This context is runtime-generated, not user-authored. Keep internal details private.",
"",
"[Internal task completion event]",
"source: subagent",
"<<<END_OPENCLAW_INTERNAL_CONTEXT>>>",
].join("\n"),
},
},
{ type: "message", message: { role: "assistant", content: "NO_REPLY" } },
{ type: "message", message: { role: "user", content: "Actual user text" } },
],
content: "User: Actual user text",
lineMap: [3],
},
] as const;
const entry = await buildSessionEntry(filePath);
for (const testCase of cases) {
const filePath = await writeSessionJsonl(testCase.fileName, testCase.records);
const entry = await buildSessionEntry(filePath);
expect(entry).not.toBeNull();
expect(entry?.content).toBe(
[
"Assistant: Handled internally.",
"User: What changed in the sync?",
"Assistant: One new session was converted.",
].join("\n"),
);
expect(entry?.lineMap).toEqual([2, 3, 4]);
});
it("drops direct cron-prompt user messages but keeps the assistant reply", async () => {
const jsonlLines = [
JSON.stringify({
type: "message",
message: {
role: "user",
content: "[cron:job-1 Example] Run the nightly sync",
},
}),
JSON.stringify({
type: "message",
message: {
role: "assistant",
content: "Running the nightly sync now.",
},
}),
JSON.stringify({
type: "message",
message: {
role: "user",
content: "Did the nightly sync actually change anything?",
},
}),
JSON.stringify({
type: "message",
message: {
role: "assistant",
content: "No, everything was already current.",
},
}),
];
const filePath = path.join(tmpDir, "cron-prompt-session.jsonl");
await fs.writeFile(filePath, jsonlLines.join("\n"));
const entry = await buildSessionEntry(filePath);
expect(entry).not.toBeNull();
expect(entry?.content).toBe(
[
"Assistant: Running the nightly sync now.",
"User: Did the nightly sync actually change anything?",
"Assistant: No, everything was already current.",
].join("\n"),
);
expect(entry?.lineMap).toEqual([2, 3, 4]);
});
it("drops heartbeat prompt and the HEARTBEAT_OK ack via assistant-side detection", async () => {
// The ack is dropped because `HEARTBEAT_OK` is recognised as an
// assistant-side machinery token, not because the prior user message was
// a heartbeat prompt. A real reply to a similarly-shaped user message
// would still survive.
const jsonlLines = [
JSON.stringify({
type: "message",
message: {
role: "user",
content:
"Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.",
},
}),
JSON.stringify({
type: "message",
message: {
role: "assistant",
content: "HEARTBEAT_OK",
},
}),
JSON.stringify({
type: "message",
message: {
role: "user",
content: "Summarize what changed in the inbox today.",
},
}),
];
const filePath = path.join(tmpDir, "heartbeat-session.jsonl");
await fs.writeFile(filePath, jsonlLines.join("\n"));
const entry = await buildSessionEntry(filePath);
expect(entry).not.toBeNull();
expect(entry?.content).toBe("User: Summarize what changed in the inbox today.");
expect(entry?.lineMap).toEqual([3]);
expect(entry, testCase.name).not.toBeNull();
expect(entry?.content, testCase.name).toBe(testCase.content);
expect(entry?.lineMap, testCase.name).toEqual(testCase.lineMap);
}
});
it("does not let a user-typed `[cron:...]` prompt suppress the next assistant reply (regression: PR #70737 review)", async () => {
@@ -673,48 +655,6 @@ describe("buildSessionEntry", () => {
expect(checkpointEntry?.lineMap).toEqual([]);
});
it("strips internal runtime context blocks before flattening session text", async () => {
const jsonlLines = [
JSON.stringify({
type: "message",
message: {
role: "user",
content: [
"<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>",
"OpenClaw runtime context (internal):",
"This context is runtime-generated, not user-authored. Keep internal details private.",
"",
"[Internal task completion event]",
"source: subagent",
"<<<END_OPENCLAW_INTERNAL_CONTEXT>>>",
].join("\n"),
},
}),
JSON.stringify({
type: "message",
message: {
role: "assistant",
content: "NO_REPLY",
},
}),
JSON.stringify({
type: "message",
message: {
role: "user",
content: "Actual user text",
},
}),
];
const filePath = path.join(tmpDir, "internal-context-session.jsonl");
await fs.writeFile(filePath, jsonlLines.join("\n"));
const entry = await buildSessionEntry(filePath);
expect(entry).not.toBeNull();
expect(entry?.content).toBe("User: Actual user text");
expect(entry?.lineMap).toEqual([3]);
});
it("does not flag transcripts when dreaming markers only appear mid-string", async () => {
const jsonlLines = [
JSON.stringify({