Skip to content

Commit d519bbe

Browse files
fix(clawsweeper): address review for automerge-openclaw-openclaw-70464 (1)
1 parent a9a4847 commit d519bbe

2 files changed

Lines changed: 205 additions & 111 deletions

File tree

extensions/memory-core/src/dreaming-narrative.test.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import { createMemoryCoreTestHarness } from "./test-helpers.js";
2929

3030
const { createTempWorkspace } = createMemoryCoreTestHarness();
3131
const DREAMS_FILE_LOCKS_KEY = Symbol.for("openclaw.memoryCore.dreamingNarrative.fileLocks");
32+
const NARRATIVE_SESSION_LOCKS_KEY = Symbol.for(
33+
"openclaw.memoryCore.dreamingNarrative.sessionLocks",
34+
);
3235
const EXPECTS_POSIX_PRIVATE_FILE_MODE = process.platform !== "win32";
3336

3437
type MockCallSource = { mock: { calls: Array<Array<unknown>> } };
@@ -80,6 +83,7 @@ async function expectPathMissing(targetPath: string): Promise<void> {
8083
afterEach(() => {
8184
vi.restoreAllMocks();
8285
resolveGlobalMap<string, unknown>(DREAMS_FILE_LOCKS_KEY).clear();
86+
resolveGlobalMap<string, unknown>(NARRATIVE_SESSION_LOCKS_KEY).clear();
8387
});
8488

8589
describe("buildNarrativePrompt", () => {
@@ -1115,10 +1119,12 @@ describe("runDetachedDreamNarrative", () => {
11151119

11161120
it("caps the number of in-flight detached narratives at 3", async () => {
11171121
const { subagent, runDeferreds } = createBlockingSubagent();
1118-
const workspaceDir = await createTempWorkspace("openclaw-dreaming-detach-");
1122+
const workspaceDirs = await Promise.all(
1123+
Array.from({ length: 5 }, () => createTempWorkspace("openclaw-dreaming-detach-")),
1124+
);
11191125
const logger = createMockLogger();
11201126

1121-
for (let i = 0; i < 5; i += 1) {
1127+
for (const [i, workspaceDir] of workspaceDirs.entries()) {
11221128
runDetachedDreamNarrative({
11231129
subagent,
11241130
workspaceDir,
@@ -1158,6 +1164,64 @@ describe("runDetachedDreamNarrative", () => {
11581164
expect(subagent.waitForRun).toHaveBeenCalledTimes(5);
11591165
});
11601166

1167+
it("serializes detached narratives that reuse a workspace and phase session", async () => {
1168+
let nextRunId = 0;
1169+
const waitDeferreds: Array<Deferred<{ status: string }>> = [];
1170+
const subagent = {
1171+
run: vi.fn(() => {
1172+
nextRunId += 1;
1173+
return Promise.resolve({ runId: `run-${nextRunId}` });
1174+
}),
1175+
waitForRun: vi.fn(() => {
1176+
const d = deferred<{ status: string }>();
1177+
waitDeferreds.push(d);
1178+
return d.promise;
1179+
}),
1180+
getSessionMessages: vi.fn().mockResolvedValue({ messages: [] }),
1181+
deleteSession: vi.fn().mockResolvedValue(undefined),
1182+
};
1183+
const workspaceDir = await createTempWorkspace("openclaw-dreaming-detach-");
1184+
const logger = createMockLogger();
1185+
1186+
for (let i = 0; i < 5; i += 1) {
1187+
runDetachedDreamNarrative({
1188+
subagent,
1189+
workspaceDir,
1190+
data: { phase: "light", snippets: [`fragment-${i}`] },
1191+
nowMs: Date.parse("2026-04-28T03:00:00Z"),
1192+
logger,
1193+
});
1194+
}
1195+
1196+
await vi.waitFor(() => {
1197+
expect(waitDeferreds.length).toBe(1);
1198+
});
1199+
1200+
expect(subagent.run).toHaveBeenCalledTimes(1);
1201+
expect(subagent.waitForRun).toHaveBeenCalledTimes(1);
1202+
// The first run is still active, so later same-key jobs must not pre-delete its session.
1203+
expect(subagent.deleteSession).toHaveBeenCalledTimes(1);
1204+
1205+
for (let i = 0; i < 5; i += 1) {
1206+
const currentDeferred = waitDeferreds[i];
1207+
if (!currentDeferred) {
1208+
throw new Error(`Expected wait deferred ${i} to exist`);
1209+
}
1210+
currentDeferred.resolve({ status: "timeout" });
1211+
if (i < 4) {
1212+
await vi.waitFor(() => {
1213+
expect(waitDeferreds.length).toBeGreaterThan(i + 1);
1214+
});
1215+
}
1216+
}
1217+
1218+
await vi.waitFor(() => {
1219+
expect(subagent.deleteSession).toHaveBeenCalledTimes(10);
1220+
});
1221+
expect(subagent.run).toHaveBeenCalledTimes(5);
1222+
expect(subagent.waitForRun).toHaveBeenCalledTimes(5);
1223+
});
1224+
11611225
it("swallows underlying narrative errors instead of leaving an unhandled rejection", async () => {
11621226
const error = new Error("boom");
11631227
const subagent = {

0 commit comments

Comments
 (0)