fix: reconcile memfs/standard prompt sections safely (#985)
This commit is contained in:
98
src/tests/agent/memoryPrompt.integration.test.ts
Normal file
98
src/tests/agent/memoryPrompt.integration.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { getClient } from "../../agent/client";
|
||||
import { createAgent } from "../../agent/create";
|
||||
import { updateAgentSystemPromptMemfs } from "../../agent/modify";
|
||||
import {
|
||||
SYSTEM_PROMPT_MEMFS_ADDON,
|
||||
SYSTEM_PROMPT_MEMORY_ADDON,
|
||||
} from "../../agent/promptAssets";
|
||||
|
||||
const describeIntegration = process.env.LETTA_API_KEY
|
||||
? describe
|
||||
: describe.skip;
|
||||
|
||||
function expectedPrompt(base: string, addon: string): string {
|
||||
return `${base.trimEnd()}\n\n${addon.trimStart()}`.trim();
|
||||
}
|
||||
|
||||
describeIntegration("memory prompt integration", () => {
|
||||
const createdAgentIds: string[] = [];
|
||||
|
||||
beforeAll(() => {
|
||||
// Avoid polluting user's normal local LRU state in integration runs.
|
||||
process.env.LETTA_CODE_AGENT_ROLE = "subagent";
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const client = await getClient();
|
||||
for (const agentId of createdAgentIds) {
|
||||
try {
|
||||
await client.agents.delete(agentId);
|
||||
} catch {
|
||||
// Best-effort cleanup.
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
"new agent prompt is exact for memfs enabled and disabled modes",
|
||||
async () => {
|
||||
const base = [
|
||||
"You are a test agent.",
|
||||
"Follow user instructions precisely.",
|
||||
].join("\n");
|
||||
|
||||
const created = await createAgent({
|
||||
name: `prompt-memfs-${Date.now()}`,
|
||||
systemPromptCustom: base,
|
||||
memoryPromptMode: "memfs",
|
||||
});
|
||||
createdAgentIds.push(created.agent.id);
|
||||
|
||||
const client = await getClient();
|
||||
|
||||
const expectedMemfs = expectedPrompt(base, SYSTEM_PROMPT_MEMFS_ADDON);
|
||||
let fetched = await client.agents.retrieve(created.agent.id);
|
||||
expect(fetched.system).toBe(expectedMemfs);
|
||||
expect((fetched.system.match(/## Memory Filesystem/g) || []).length).toBe(
|
||||
1,
|
||||
);
|
||||
expect((fetched.system.match(/# See what changed/g) || []).length).toBe(
|
||||
1,
|
||||
);
|
||||
|
||||
const enableAgain = await updateAgentSystemPromptMemfs(
|
||||
created.agent.id,
|
||||
true,
|
||||
);
|
||||
expect(enableAgain.success).toBe(true);
|
||||
fetched = await client.agents.retrieve(created.agent.id);
|
||||
expect(fetched.system).toBe(expectedMemfs);
|
||||
|
||||
const disable = await updateAgentSystemPromptMemfs(
|
||||
created.agent.id,
|
||||
false,
|
||||
);
|
||||
expect(disable.success).toBe(true);
|
||||
const expectedStandard = expectedPrompt(base, SYSTEM_PROMPT_MEMORY_ADDON);
|
||||
fetched = await client.agents.retrieve(created.agent.id);
|
||||
expect(fetched.system).toBe(expectedStandard);
|
||||
expect(fetched.system).not.toContain("## Memory Filesystem");
|
||||
expect(fetched.system).toContain(
|
||||
"Your memory consists of core memory (composed of memory blocks)",
|
||||
);
|
||||
|
||||
const reEnable = await updateAgentSystemPromptMemfs(
|
||||
created.agent.id,
|
||||
true,
|
||||
);
|
||||
expect(reEnable.success).toBe(true);
|
||||
fetched = await client.agents.retrieve(created.agent.id);
|
||||
expect(fetched.system).toBe(expectedMemfs);
|
||||
expect((fetched.system.match(/# See what changed/g) || []).length).toBe(
|
||||
1,
|
||||
);
|
||||
},
|
||||
{ timeout: 120000 },
|
||||
);
|
||||
});
|
||||
70
src/tests/agent/memoryPrompt.test.ts
Normal file
70
src/tests/agent/memoryPrompt.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import {
|
||||
detectMemoryPromptDrift,
|
||||
reconcileMemoryPrompt,
|
||||
} from "../../agent/memoryPrompt";
|
||||
import {
|
||||
SYSTEM_PROMPT_MEMFS_ADDON,
|
||||
SYSTEM_PROMPT_MEMORY_ADDON,
|
||||
} from "../../agent/promptAssets";
|
||||
|
||||
function countOccurrences(haystack: string, needle: string): number {
|
||||
if (!needle) return 0;
|
||||
return haystack.split(needle).length - 1;
|
||||
}
|
||||
|
||||
describe("memoryPrompt reconciler", () => {
|
||||
test("replaces existing standard memory section with memfs section", () => {
|
||||
const base = "You are a test agent.";
|
||||
const standard = `${base}\n\n${SYSTEM_PROMPT_MEMORY_ADDON.trimStart()}`;
|
||||
|
||||
const reconciled = reconcileMemoryPrompt(standard, "memfs");
|
||||
|
||||
expect(reconciled).toContain("## Memory Filesystem");
|
||||
expect(reconciled).not.toContain(
|
||||
"Your memory consists of core memory (composed of memory blocks)",
|
||||
);
|
||||
expect(countOccurrences(reconciled, "## Memory Filesystem")).toBe(1);
|
||||
});
|
||||
|
||||
test("does not leave orphan memfs sync fragment when switching from memfs to standard", () => {
|
||||
const base = "You are a test agent.";
|
||||
const memfs = `${base}\n\n${SYSTEM_PROMPT_MEMFS_ADDON.trimStart()}`;
|
||||
|
||||
const reconciled = reconcileMemoryPrompt(memfs, "standard");
|
||||
|
||||
expect(reconciled).toContain(
|
||||
"Your memory consists of core memory (composed of memory blocks)",
|
||||
);
|
||||
expect(reconciled).not.toContain("## Memory Filesystem");
|
||||
expect(reconciled).not.toContain("# See what changed");
|
||||
expect(reconciled).not.toContain('git commit -m "<type>: <what changed>"');
|
||||
});
|
||||
|
||||
test("cleans orphan memfs tail fragment before rebuilding target mode", () => {
|
||||
const tailStart = SYSTEM_PROMPT_MEMFS_ADDON.indexOf("# See what changed");
|
||||
expect(tailStart).toBeGreaterThanOrEqual(0);
|
||||
const orphanTail = SYSTEM_PROMPT_MEMFS_ADDON.slice(tailStart).trim();
|
||||
|
||||
const drifted = `Header text\n\n${orphanTail}`;
|
||||
const drifts = detectMemoryPromptDrift(drifted, "standard");
|
||||
expect(drifts.some((d) => d.code === "orphan_memfs_fragment")).toBe(true);
|
||||
|
||||
const reconciled = reconcileMemoryPrompt(drifted, "standard");
|
||||
expect(reconciled).toContain(
|
||||
"Your memory consists of core memory (composed of memory blocks)",
|
||||
);
|
||||
expect(reconciled).not.toContain("# See what changed");
|
||||
});
|
||||
|
||||
test("memfs reconciliation is idempotent and keeps single syncing section", () => {
|
||||
const base = "You are a test agent.";
|
||||
const once = reconcileMemoryPrompt(base, "memfs");
|
||||
const twice = reconcileMemoryPrompt(once, "memfs");
|
||||
|
||||
expect(twice).toBe(once);
|
||||
expect(countOccurrences(twice, "## Syncing")).toBe(1);
|
||||
expect(countOccurrences(twice, "# See what changed")).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user