





















@@ -76,6 +76,41 @@ function requireCandidateKeyByPath(
7676return key;
7777}
787879+function mockStringMessages(mock: { mock: { calls: unknown[][] } }): string[] {
80+return mock.mock.calls.map((call) => String(call[0] ?? ""));
81+}
82+83+function expectIncludesSubstring(values: readonly string[], expected: string): void {
84+expect(values.some((value) => value.includes(expected))).toBe(true);
85+}
86+87+function expectNotIncludesSubstring(values: readonly string[], expected: string): void {
88+expect(values.every((value) => !value.includes(expected))).toBe(true);
89+}
90+91+async function expectPathMissing(targetPath: string): Promise<void> {
92+try {
93+await fs.access(targetPath);
94+} catch (error) {
95+if (error && typeof error === "object" && "code" in error) {
96+expect(error.code).toBe("ENOENT");
97+return;
98+}
99+throw error;
100+}
101+throw new Error(`expected path to be missing: ${targetPath}`);
102+}
103+104+function requireFirstIngestionEntry(sessionIngestion: {
105+files: Record<string, { lineCount: number; lastContentLine: number; contentHash: string }>;
106+}) {
107+const firstEntry = Object.values(sessionIngestion.files)[0];
108+if (!firstEntry) {
109+throw new Error("expected session ingestion entry");
110+}
111+return firstEntry;
112+}
113+79114function createHarness(
80115config: OpenClawConfig,
81116workspaceDir?: string,
@@ -347,11 +382,9 @@ describe("memory-core dreaming phases", () => {
347382const dreams = await fs.readFile(path.join(workspaceDir, "DREAMS.md"), "utf-8");
348383expect(dreams).toContain("Move backups to S3 Glacier.");
349384expect(logger.error).not.toHaveBeenCalled();
350-expect(logger.info).toHaveBeenCalledWith(expect.stringContaining("request-scoped"));
351-expect(logger.warn).not.toHaveBeenCalledWith(expect.stringContaining("request-scoped"));
352-expect(logger.warn).not.toHaveBeenCalledWith(
353-expect.stringContaining("narrative session cleanup failed"),
354-);
385+expectIncludesSubstring(mockStringMessages(logger.info), "request-scoped");
386+expectNotIncludesSubstring(mockStringMessages(logger.warn), "request-scoped");
387+expectNotIncludesSubstring(mockStringMessages(logger.warn), "narrative session cleanup failed");
355388expect(subagent.deleteSession).not.toHaveBeenCalled();
356389});
357390@@ -705,12 +738,9 @@ describe("memory-core dreaming phases", () => {
705738expect(ranked.map((candidate) => candidate.path)).toContain(
706739"memory/.dreams/session-corpus/2026-04-05.txt",
707740);
708-expect(ranked.map((candidate) => candidate.snippet)).toEqual(
709-expect.arrayContaining([
710-expect.stringContaining("Move backups to S3 Glacier."),
711-expect.stringContaining("Set retention to 365 days."),
712-]),
713-);
741+const snippets = ranked.map((candidate) => candidate.snippet);
742+expectIncludesSubstring(snippets, "Move backups to S3 Glacier.");
743+expectIncludesSubstring(snippets, "Set retention to 365 days.");
714744});
715745716746it("keeps primary session transcripts out of configured subagent workspaces", async () => {
@@ -959,9 +989,9 @@ describe("memory-core dreaming phases", () => {
959989vi.unstubAllEnvs();
960990}
961991962-await expect(
963-fs.access(path.join(workspaceDir, "memory", ".dreams", "session-corpus", "2026-04-05.txt")),
964-).rejects.toMatchObject({ code: "ENOENT" });
992+await expectPathMissing(
993+path.join(workspaceDir, "memory", ".dreams", "session-corpus", "2026-04-05.txt"),
994+);
965995966996const sessionIngestion = JSON.parse(
967997await fs.readFile(
@@ -979,13 +1009,10 @@ describe("memory-core dreaming phases", () => {
9791009>;
9801010};
9811011expect(Object.keys(sessionIngestion.files)).toHaveLength(1);
982-expect(Object.values(sessionIngestion.files)).toEqual([
983-expect.objectContaining({
984-lineCount: 0,
985-lastContentLine: 0,
986-contentHash: EMPTY_SESSION_CONTENT_HASH,
987-}),
988-]);
1012+const ingestionEntry = requireFirstIngestionEntry(sessionIngestion);
1013+expect(ingestionEntry.lineCount).toBe(0);
1014+expect(ingestionEntry.lastContentLine).toBe(0);
1015+expect(ingestionEntry.contentHash).toBe(EMPTY_SESSION_CONTENT_HASH);
9891016});
99010179911018it("skips dreaming transcripts when the session store identifies them before bootstrap lands", async () => {
@@ -1072,9 +1099,9 @@ describe("memory-core dreaming phases", () => {
10721099vi.unstubAllEnvs();
10731100}
107411011075-await expect(
1076-fs.access(path.join(workspaceDir, "memory", ".dreams", "session-corpus", "2026-04-05.txt")),
1077-).rejects.toMatchObject({ code: "ENOENT" });
1102+await expectPathMissing(
1103+path.join(workspaceDir, "memory", ".dreams", "session-corpus", "2026-04-05.txt"),
1104+);
1078110510791106const sessionIngestion = JSON.parse(
10801107await fs.readFile(
@@ -1092,13 +1119,10 @@ describe("memory-core dreaming phases", () => {
10921119>;
10931120};
10941121expect(Object.keys(sessionIngestion.files)).toHaveLength(1);
1095-expect(Object.values(sessionIngestion.files)).toEqual([
1096-expect.objectContaining({
1097-lineCount: 0,
1098-lastContentLine: 0,
1099-contentHash: EMPTY_SESSION_CONTENT_HASH,
1100-}),
1101-]);
1122+const ingestionEntry = requireFirstIngestionEntry(sessionIngestion);
1123+expect(ingestionEntry.lineCount).toBe(0);
1124+expect(ingestionEntry.lastContentLine).toBe(0);
1125+expect(ingestionEntry.contentHash).toBe(EMPTY_SESSION_CONTENT_HASH);
11021126});
1103112711041128it("skips isolated cron run transcripts during session ingestion", async () => {
@@ -1182,9 +1206,9 @@ describe("memory-core dreaming phases", () => {
11821206vi.unstubAllEnvs();
11831207}
118412081185-await expect(
1186-fs.access(path.join(workspaceDir, "memory", ".dreams", "session-corpus", "2026-04-05.txt")),
1187-).rejects.toMatchObject({ code: "ENOENT" });
1209+await expectPathMissing(
1210+path.join(workspaceDir, "memory", ".dreams", "session-corpus", "2026-04-05.txt"),
1211+);
1188121211891213const sessionIngestion = JSON.parse(
11901214await fs.readFile(
@@ -1201,13 +1225,10 @@ describe("memory-core dreaming phases", () => {
12011225}
12021226>;
12031227};
1204-expect(Object.values(sessionIngestion.files)).toEqual([
1205-expect.objectContaining({
1206-lineCount: 0,
1207-lastContentLine: 0,
1208-contentHash: EMPTY_SESSION_CONTENT_HASH,
1209-}),
1210-]);
1228+const ingestionEntry = requireFirstIngestionEntry(sessionIngestion);
1229+expect(ingestionEntry.lineCount).toBe(0);
1230+expect(ingestionEntry.lastContentLine).toBe(0);
1231+expect(ingestionEntry.contentHash).toBe(EMPTY_SESSION_CONTENT_HASH);
12111232});
1212123312131234it("drops generated system wrapper text without suppressing paired assistant replies", async () => {
@@ -1958,12 +1979,9 @@ describe("memory-core dreaming phases", () => {
19581979minUniqueQueries: 0,
19591980nowMs: Date.parse("2026-04-06T02:00:00.000Z"),
19601981});
1961-expect(ranked.map((candidate) => candidate.snippet)).toEqual(
1962-expect.arrayContaining([
1963-expect.stringContaining("Move backups to S3 Glacier."),
1964-expect.stringContaining("Retention policy stays at 365 days."),
1965-]),
1966-);
1982+const snippets = ranked.map((candidate) => candidate.snippet);
1983+expectIncludesSubstring(snippets, "Move backups to S3 Glacier.");
1984+expectIncludesSubstring(snippets, "Retention policy stays at 365 days.");
19671985});
1968198619691987it("ingests sessions when dreaming is enabled even if memorySearch is disabled", async () => {
@@ -2037,10 +2055,9 @@ describe("memory-core dreaming phases", () => {
20372055minUniqueQueries: 0,
20382056nowMs: Date.parse("2026-04-05T19:00:00.000Z"),
20392057});
2040-expect(ranked.map((candidate) => candidate.snippet)).toEqual(
2041-expect.arrayContaining([
2042-expect.stringContaining("Glacier archive migration is now complete."),
2043-]),
2058+expectIncludesSubstring(
2059+ranked.map((candidate) => candidate.snippet),
2060+"Glacier archive migration is now complete.",
20442061);
20452062});
20462063@@ -2155,12 +2172,9 @@ describe("memory-core dreaming phases", () => {
21552172nowMs: Date.parse("2026-04-05T10:05:00.000Z"),
21562173});
21572174expect(after).toHaveLength(2);
2158-expect(after.map((candidate) => candidate.snippet)).toEqual(
2159-expect.arrayContaining([
2160-"Reviewed travel timing and calendar placement.",
2161-expect.stringContaining("Emma Rees:"),
2162-]),
2163-);
2175+const snippets = after.map((candidate) => candidate.snippet);
2176+expect(snippets).toContain("Reviewed travel timing and calendar placement.");
2177+expectIncludesSubstring(snippets, "Emma Rees:");
21642178for (const candidate of after) {
21652179expect(candidate.snippet).not.toContain("Friday, April 5, 2026:");
21662180expect(candidate.snippet).not.toContain("Morning:");
@@ -2226,17 +2240,16 @@ describe("memory-core dreaming phases", () => {
22262240nowMs: Date.parse("2026-04-05T10:05:00.000Z"),
22272241});
22282242expect(after).toHaveLength(3);
2229-expect(after.map((candidate) => candidate.snippet)).toEqual(
2230-expect.arrayContaining([
2231-expect.stringContaining(
2232-"Operations: Restarted the gateway after auth drift.; Tokens now line up again.",
2233-),
2234-expect.stringContaining(
2235-"Bex: She prefers direct plans over open-ended maybes.; Better to offer one concrete time window.",
2236-),
2237-expect.stringContaining("Travel: Flight lands at 08:10."),
2238-]),
2243+const snippets = after.map((candidate) => candidate.snippet);
2244+expectIncludesSubstring(
2245+snippets,
2246+"Operations: Restarted the gateway after auth drift.; Tokens now line up again.",
22392247);
2248+expectIncludesSubstring(
2249+snippets,
2250+"Bex: She prefers direct plans over open-ended maybes.; Better to offer one concrete time window.",
2251+);
2252+expectIncludesSubstring(snippets, "Travel: Flight lands at 08:10.");
22402253});
2241225422422255it("records light/rem signals that reinforce deep promotion ranking", async () => {
@@ -2343,10 +2356,9 @@ describe("memory-core dreaming phases", () => {
23432356const phaseSignalStore = JSON.parse(await fs.readFile(phaseSignalPath, "utf-8")) as {
23442357entries: Record<string, { lightHits: number; remHits: number }>;
23452358};
2346-expect(phaseSignalStore.entries[baseline[0].key]).toMatchObject({
2347-lightHits: 1,
2348-remHits: 1,
2349-});
2359+const baselineSignals = phaseSignalStore.entries[baseline[0].key];
2360+expect(baselineSignals?.lightHits).toBe(1);
2361+expect(baselineSignals?.remHits).toBe(1);
23502362});
2351236323522364it("skips REM short-term candidates whose source file disappeared", async () => {
@@ -2429,7 +2441,7 @@ describe("memory-core dreaming phases", () => {
24292441const phaseSignalStore = JSON.parse(await fs.readFile(phaseSignalPath, "utf-8")) as {
24302442entries: Record<string, { remHits: number }>;
24312443};
2432-expect(phaseSignalStore.entries[liveKey]).toMatchObject({ remHits: 1 });
2444+expect(phaseSignalStore.entries[liveKey]?.remHits).toBe(1);
24332445expect(phaseSignalStore.entries[staleKey]).toBeUndefined();
2434244624352447const remOutput = await fs.readFile(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。