






















@@ -31,6 +31,52 @@ const { createTempWorkspace } = createMemoryCoreTestHarness();
3131const DREAMS_FILE_LOCKS_KEY = Symbol.for("openclaw.memoryCore.dreamingNarrative.fileLocks");
3232const EXPECTS_POSIX_PRIVATE_FILE_MODE = process.platform !== "win32";
333334+type MockCallSource = { mock: { calls: Array<Array<unknown>> } };
35+36+function mockCallArg(source: MockCallSource, label: string, callIndex = 0, argIndex = 0): unknown {
37+const call = source.mock.calls[callIndex];
38+if (!call) {
39+throw new Error(`Expected ${label} call ${callIndex} to exist`);
40+}
41+if (!(argIndex in call)) {
42+throw new Error(`Expected ${label} call ${callIndex} argument ${argIndex} to exist`);
43+}
44+return call[argIndex];
45+}
46+47+function mockObjectArg(
48+source: MockCallSource,
49+label: string,
50+callIndex = 0,
51+argIndex = 0,
52+): Record<string, unknown> {
53+const value = mockCallArg(source, label, callIndex, argIndex);
54+if (!value || typeof value !== "object") {
55+throw new Error(`Expected ${label} call ${callIndex} argument ${argIndex} to be an object`);
56+}
57+return value as Record<string, unknown>;
58+}
59+60+function logIncludes(source: MockCallSource, text: string): boolean {
61+return source.mock.calls.some((call) => String(call[0]).includes(text));
62+}
63+64+function expectLogIncludes(source: MockCallSource, text: string): void {
65+expect(logIncludes(source, text), `Expected log to include ${text}`).toBe(true);
66+}
67+68+function expectLogExcludes(source: MockCallSource, text: string): void {
69+expect(logIncludes(source, text), `Expected log not to include ${text}`).toBe(false);
70+}
71+72+async function expectPathMissing(targetPath: string): Promise<void> {
73+const accessResult = await fs
74+.access(targetPath)
75+.then(() => "exists")
76+.catch((error: unknown) => (error as { code?: unknown }).code);
77+expect(accessResult).toBe("ENOENT");
78+}
79+3480afterEach(() => {
3581vi.restoreAllMocks();
3682resolveGlobalMap<string, unknown>(DREAMS_FILE_LOCKS_KEY).clear();
@@ -617,14 +663,13 @@ describe("generateAndAppendDreamNarrative", () => {
617663});
618664619665expect(subagent.run).toHaveBeenCalledOnce();
620-expect(subagent.run.mock.calls[0][0]).toMatchObject({
621-idempotencyKey: expectedSessionKey,
622-sessionKey: expectedSessionKey,
623-lane: `dreaming-narrative:${expectedSessionKey}`,
624-lightContext: true,
625-deliver: false,
626-model: "anthropic/claude-sonnet-4-6",
627-});
666+const runOptions = mockObjectArg(subagent.run, "subagent run");
667+expect(runOptions.idempotencyKey).toBe(expectedSessionKey);
668+expect(runOptions.sessionKey).toBe(expectedSessionKey);
669+expect(runOptions.lane).toBe(`dreaming-narrative:${expectedSessionKey}`);
670+expect(runOptions.lightContext).toBe(true);
671+expect(runOptions.deliver).toBe(false);
672+expect(runOptions.model).toBe("anthropic/claude-sonnet-4-6");
628673expect(subagent.waitForRun).toHaveBeenCalledOnce();
629674expect(subagent.deleteSession).toHaveBeenCalledOnce();
630675@@ -657,21 +702,19 @@ describe("generateAndAppendDreamNarrative", () => {
657702});
658703659704expect(subagent.run).toHaveBeenCalledTimes(2);
660-expect(subagent.run.mock.calls[0]?.[0]).toMatchObject({
661-sessionKey: expectedSessionKey,
662-model: "ollama/missing-model",
663-});
664-expect(subagent.run.mock.calls[1]?.[0]).toMatchObject({
665-sessionKey: retrySessionKey,
666-});
667-expect(subagent.run.mock.calls[1]?.[0]).not.toHaveProperty("model");
705+const configuredRunOptions = mockObjectArg(subagent.run, "subagent run");
706+expect(configuredRunOptions.sessionKey).toBe(expectedSessionKey);
707+expect(configuredRunOptions.model).toBe("ollama/missing-model");
708+const retryRunOptions = mockObjectArg(subagent.run, "subagent run", 1);
709+expect(retryRunOptions.sessionKey).toBe(retrySessionKey);
710+expect(retryRunOptions).not.toHaveProperty("model");
668711expect(subagent.getSessionMessages).toHaveBeenCalledWith({
669712sessionKey: retrySessionKey,
670713limit: 5,
671714});
672715expect(subagent.deleteSession).toHaveBeenCalledOnce();
673716expect(subagent.deleteSession).toHaveBeenCalledWith({ sessionKey: retrySessionKey });
674-expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining("session default"));
717+expectLogIncludes(logger.warn, "session default");
675718});
676719677720it("retries with the session default when the configured model run ends unavailable", async () => {
@@ -710,7 +753,7 @@ describe("generateAndAppendDreamNarrative", () => {
710753expect(subagent.deleteSession).toHaveBeenCalledTimes(2);
711754expect(subagent.deleteSession.mock.calls[0]?.[0]).toEqual({ sessionKey: expectedSessionKey });
712755expect(subagent.deleteSession.mock.calls[1]?.[0]).toEqual({ sessionKey: retrySessionKey });
713-expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining("unknown model"));
756+expectLogIncludes(logger.warn, "unknown model");
714757});
715758716759it("does not hide configured model authorization failures by retrying", async () => {
@@ -735,9 +778,7 @@ describe("generateAndAppendDreamNarrative", () => {
735778expect(subagent.run).toHaveBeenCalledOnce();
736779expect(subagent.waitForRun).not.toHaveBeenCalled();
737780expect(subagent.deleteSession).not.toHaveBeenCalled();
738-expect(logger.warn).toHaveBeenCalledWith(
739-expect.stringContaining("narrative generation failed"),
740-);
781+expectLogIncludes(logger.warn, "narrative generation failed");
741782});
742783743784it("skips narrative when no snippets are available", async () => {
@@ -797,10 +838,8 @@ describe("generateAndAppendDreamNarrative", () => {
797838});
798839799840expect(subagent.waitForRun).toHaveBeenCalledOnce();
800-expect(subagent.waitForRun.mock.calls[0][0]).toMatchObject({ timeoutMs: 60_000 });
801-expect(logger.warn).toHaveBeenCalledWith(
802-expect.stringContaining("narrative session cleanup failed for rem phase"),
803-);
841+expect(mockObjectArg(subagent.waitForRun, "wait for run").timeoutMs).toBe(60_000);
842+expectLogIncludes(logger.warn, "narrative session cleanup failed for rem phase");
804843});
805844806845it("handles subagent error gracefully", async () => {
@@ -822,9 +861,7 @@ describe("generateAndAppendDreamNarrative", () => {
822861823862// Should not throw.
824863expect(logger.warn).toHaveBeenCalled();
825-await expect(fs.access(path.join(workspaceDir, "DREAMS.md"))).rejects.toMatchObject({
826-code: "ENOENT",
827-});
864+await expectPathMissing(path.join(workspaceDir, "DREAMS.md"));
828865});
829866830867it("falls back to a local narrative when subagent runtime is request-scoped", async () => {
@@ -844,12 +881,10 @@ describe("generateAndAppendDreamNarrative", () => {
844881845882const content = await fs.readFile(path.join(workspaceDir, "DREAMS.md"), "utf-8");
846883expect(content).toContain("API endpoints need authentication");
847-expect(logger.info).toHaveBeenCalledWith(expect.stringContaining("request-scoped"));
848-expect(logger.warn).not.toHaveBeenCalledWith(expect.stringContaining("request-scoped"));
849-expect(logger.warn).not.toHaveBeenCalledWith(expect.stringContaining(workspaceDir));
850-expect(logger.warn).not.toHaveBeenCalledWith(
851-expect.stringContaining("narrative session cleanup failed"),
852-);
884+expectLogIncludes(logger.info, "request-scoped");
885+expectLogExcludes(logger.warn, "request-scoped");
886+expectLogExcludes(logger.warn, workspaceDir);
887+expectLogExcludes(logger.warn, "narrative session cleanup failed");
853888expect(subagent.deleteSession).not.toHaveBeenCalled();
854889});
855890@@ -875,8 +910,8 @@ describe("generateAndAppendDreamNarrative", () => {
875910876911const content = await fs.readFile(path.join(workspaceDir, "DREAMS.md"), "utf-8");
877912expect(content).toContain("A durable candidate surfaced.");
878-expect(logger.info).toHaveBeenCalledWith(expect.stringContaining("request-scoped"));
879-expect(logger.warn).not.toHaveBeenCalledWith(expect.stringContaining("request-scoped"));
913+expectLogIncludes(logger.info, "request-scoped");
914+expectLogExcludes(logger.warn, "request-scoped");
880915expect(subagent.deleteSession).not.toHaveBeenCalled();
881916});
882917@@ -897,12 +932,8 @@ describe("generateAndAppendDreamNarrative", () => {
897932 logger,
898933});
899934900-await expect(fs.access(path.join(workspaceDir, "DREAMS.md"))).rejects.toMatchObject({
901-code: "ENOENT",
902-});
903-expect(logger.warn).toHaveBeenCalledWith(
904-expect.stringContaining("narrative generation failed"),
905-);
935+await expectPathMissing(path.join(workspaceDir, "DREAMS.md"));
936+expectLogIncludes(logger.warn, "narrative generation failed");
906937});
907938908939it("cleans up session even on failure", async () => {
@@ -982,7 +1013,7 @@ describe("generateAndAppendDreamNarrative", () => {
9821013const sessionFiles = await fs.readdir(sessionsDir);
9831014expect(sessionFiles).toContainEqual(expect.stringMatching(/^orphan\.jsonl\.deleted\./));
9841015expect(sessionFiles).toContain("still-live.jsonl");
985-expect(logger.info).toHaveBeenCalledWith(expect.stringContaining("dreaming cleanup scrubbed"));
1016+expectLogIncludes(logger.info, "dreaming cleanup scrubbed");
9861017});
98710189881019it("isolates narrative sessions across workspaces even at the same timestamp", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。