


























@@ -21,6 +21,7 @@ import {
2121formatBackfillDiaryDate,
2222generateAndAppendDreamNarrative,
2323removeBackfillDiaryEntries,
24+runDetachedDreamNarrative,
2425type NarrativePhaseData,
2526writeBackfillDiaryEntries,
2627} from "./dreaming-narrative.js";
@@ -1003,3 +1004,117 @@ describe("generateAndAppendDreamNarrative", () => {
10031004expect(subagent.deleteSession.mock.calls[1]?.[0]?.sessionKey).toBe(secondSessionKey);
10041005});
10051006});
1007+1008+describe("runDetachedDreamNarrative", () => {
1009+type Deferred<T> = { promise: Promise<T>; resolve: (v: T) => void };
1010+function deferred<T>(): Deferred<T> {
1011+let resolve!: (v: T) => void;
1012+const promise = new Promise<T>((r) => {
1013+resolve = r;
1014+});
1015+return { promise, resolve };
1016+}
1017+1018+function createBlockingSubagent() {
1019+const runDeferreds: Array<Deferred<{ runId: string }>> = [];
1020+const subagent = {
1021+run: vi.fn(() => {
1022+const d = deferred<{ runId: string }>();
1023+runDeferreds.push(d);
1024+return d.promise;
1025+}),
1026+// Resolve the rest of the pipeline as a no-op so a single resolve()
1027+// on a deferred unblocks the slot for the queued task.
1028+waitForRun: vi.fn().mockResolvedValue({ status: "timeout" }),
1029+getSessionMessages: vi.fn().mockResolvedValue({ messages: [] }),
1030+deleteSession: vi.fn().mockResolvedValue(undefined),
1031+};
1032+return { subagent, runDeferreds };
1033+}
1034+1035+function createMockLogger() {
1036+return { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
1037+}
1038+1039+async function drainMicrotasks(rounds = 30): Promise<void> {
1040+for (let i = 0; i < rounds; i += 1) {
1041+await Promise.resolve();
1042+}
1043+}
1044+1045+it("caps the number of in-flight detached narratives at 3", async () => {
1046+const { subagent, runDeferreds } = createBlockingSubagent();
1047+const workspaceDir = await createTempWorkspace("openclaw-dreaming-detach-");
1048+const logger = createMockLogger();
1049+1050+for (let i = 0; i < 5; i += 1) {
1051+runDetachedDreamNarrative({
1052+ subagent,
1053+ workspaceDir,
1054+data: { phase: "light", snippets: [`fragment-${i}`] },
1055+nowMs: Date.parse("2026-04-28T03:00:00Z"),
1056+ logger,
1057+});
1058+}
1059+1060+await drainMicrotasks();
1061+1062+// Only the first 3 should have reached subagent.run; the rest are queued.
1063+expect(subagent.run).toHaveBeenCalledTimes(3);
1064+1065+// Drain the rest so module-level concurrency state does not leak into
1066+// subsequent tests. The mock subagent creates a new deferred every time
1067+// queued tasks acquire a slot, so loop until no new deferreds appear.
1068+for (let iter = 0; iter < 10; iter += 1) {
1069+const before = runDeferreds.length;
1070+for (const d of runDeferreds) {
1071+d.resolve({ runId: "drain" });
1072+}
1073+if (before >= 5) {
1074+break;
1075+}
1076+await vi.waitFor(() => {
1077+expect(runDeferreds.length).toBeGreaterThan(before);
1078+});
1079+}
1080+for (const d of runDeferreds) {
1081+d.resolve({ runId: "drain" });
1082+}
1083+await vi.waitFor(() => {
1084+expect(subagent.deleteSession).toHaveBeenCalledTimes(5);
1085+});
1086+expect(subagent.run).toHaveBeenCalledTimes(5);
1087+expect(subagent.waitForRun).toHaveBeenCalledTimes(5);
1088+});
1089+1090+it("swallows underlying narrative errors instead of leaving an unhandled rejection", async () => {
1091+const error = new Error("boom");
1092+const subagent = {
1093+run: vi.fn().mockRejectedValue(error),
1094+waitForRun: vi.fn().mockResolvedValue({ status: "ok" }),
1095+getSessionMessages: vi.fn().mockResolvedValue({ messages: [] }),
1096+deleteSession: vi.fn().mockResolvedValue(undefined),
1097+};
1098+const logger = createMockLogger();
1099+const workspaceDir = await createTempWorkspace("openclaw-dreaming-detach-");
1100+const unhandled = vi.fn();
1101+process.on("unhandledRejection", unhandled);
1102+1103+try {
1104+runDetachedDreamNarrative({
1105+ subagent,
1106+ workspaceDir,
1107+data: { phase: "light", snippets: ["fragment"] },
1108+nowMs: Date.parse("2026-04-28T03:00:00Z"),
1109+ logger,
1110+});
1111+1112+await drainMicrotasks();
1113+1114+expect(subagent.run).toHaveBeenCalledOnce();
1115+expect(unhandled).not.toHaveBeenCalled();
1116+} finally {
1117+process.off("unhandledRejection", unhandled);
1118+}
1119+});
1120+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。