






















@@ -59,7 +59,11 @@ async function writeDailyMemoryNote(
59596060function createCronHarness(
6161initialJobs: CronJobLike[] = [],
62-opts?: { removeResult?: "boolean" | "unknown"; removeThrowsForIds?: string[] },
62+opts?: {
63+listThrowsForFirstCalls?: number;
64+removeResult?: "boolean" | "unknown";
65+removeThrowsForIds?: string[];
66+},
6367) {
6468const jobs: CronJobLike[] = [...initialJobs];
6569let listCalls = 0;
@@ -70,6 +74,9 @@ function createCronHarness(
7074const cron: CronParam = {
7175async list() {
7276listCalls += 1;
77+if (opts?.listThrowsForFirstCalls && listCalls <= opts.listThrowsForFirstCalls) {
78+throw new Error(`list failed on call ${listCalls}`);
79+}
7380return jobs.map((job) => ({
7481 ...job,
7582 ...(job.schedule ? { schedule: { ...job.schedule } } : {}),
@@ -173,13 +180,36 @@ function getGatewayStartHandler(
173180) => Promise<unknown>;
174181}
175182183+function getGatewayStopHandler(
184+onMock: ReturnType<typeof vi.fn>,
185+): (
186+event: { reason?: string },
187+ctx: { config?: OpenClawConfig; workspaceDir?: string; getCron?: () => unknown },
188+) => Promise<unknown> | void {
189+const call = onMock.mock.calls.find(([eventName]) => eventName === "gateway_stop");
190+if (!call) {
191+throw new Error("gateway_stop hook was not registered");
192+}
193+return call[1] as (
194+event: { reason?: string },
195+ctx: { config?: OpenClawConfig; workspaceDir?: string; getCron?: () => unknown },
196+) => Promise<unknown> | void;
197+}
198+176199async function triggerGatewayStart(
177200onMock: ReturnType<typeof vi.fn>,
178201ctx: { config?: OpenClawConfig; workspaceDir?: string; getCron?: () => unknown },
179202): Promise<void> {
180203await getGatewayStartHandler(onMock)({ port: 18789 }, ctx);
181204}
182205206+async function triggerGatewayStop(
207+onMock: ReturnType<typeof vi.fn>,
208+ctx: { config?: OpenClawConfig; workspaceDir?: string; getCron?: () => unknown } = {},
209+): Promise<void> {
210+await getGatewayStopHandler(onMock)({ reason: "test" }, ctx);
211+}
212+183213function registerShortTermPromotionDreamingForTest(api: DreamingPluginApiTestDouble): void {
184214registerShortTermPromotionDreaming(api as unknown as DreamingPluginApi);
185215}
@@ -1332,6 +1362,191 @@ describe("gateway startup reconciliation", () => {
13321362}
13331363});
133413641365+it("retries startup cron reconciliation until cron is available without a heartbeat (regression #72841)", async () => {
1366+vi.useFakeTimers();
1367+clearInternalHooks();
1368+const logger = createLogger();
1369+const harness = createCronHarness();
1370+const onMock = vi.fn();
1371+const api: DreamingPluginApiTestDouble = {
1372+config: {
1373+plugins: {
1374+entries: {
1375+"memory-core": {
1376+config: {
1377+dreaming: {
1378+enabled: true,
1379+frequency: "15 4 * * *",
1380+timezone: "UTC",
1381+},
1382+},
1383+},
1384+},
1385+},
1386+},
1387+pluginConfig: {},
1388+ logger,
1389+runtime: {},
1390+on: onMock,
1391+};
1392+1393+try {
1394+registerShortTermPromotionDreamingForTest(api);
1395+let cronAvailable = false;
1396+await triggerGatewayStart(onMock, {
1397+config: api.config,
1398+getCron: () => (cronAvailable ? harness.cron : undefined),
1399+});
1400+1401+expect(harness.addCalls).toHaveLength(0);
1402+expect(logger.debug).toHaveBeenCalledWith(
1403+expect.stringContaining("cron service not yet available at gateway_start"),
1404+);
1405+1406+await vi.advanceTimersByTimeAsync(constants.STARTUP_CRON_RETRY_DELAY_MS);
1407+expect(harness.addCalls).toHaveLength(0);
1408+expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining("cron service unavailable"));
1409+1410+cronAvailable = true;
1411+await vi.advanceTimersByTimeAsync(constants.STARTUP_CRON_RETRY_DELAY_MS);
1412+1413+expect(harness.addCalls).toHaveLength(1);
1414+expect(harness.addCalls[0]).toMatchObject({
1415+name: "Memory Dreaming Promotion",
1416+schedule: {
1417+kind: "cron",
1418+expr: "15 4 * * *",
1419+tz: "UTC",
1420+},
1421+sessionTarget: "isolated",
1422+payload: {
1423+kind: "agentTurn",
1424+message: constants.DREAMING_SYSTEM_EVENT_TEXT,
1425+lightContext: true,
1426+},
1427+});
1428+} finally {
1429+vi.useRealTimers();
1430+clearInternalHooks();
1431+}
1432+});
1433+1434+it("does not reschedule startup cron retry from stale enabled config after runtime config disables dreaming", async () => {
1435+vi.useFakeTimers();
1436+clearInternalHooks();
1437+const logger = createLogger();
1438+const harness = createCronHarness([], { listThrowsForFirstCalls: 1 });
1439+const onMock = vi.fn();
1440+const api: DreamingPluginApiTestDouble = {
1441+config: {
1442+plugins: {
1443+entries: {
1444+"memory-core": {
1445+config: {
1446+dreaming: {
1447+enabled: true,
1448+frequency: "15 4 * * *",
1449+timezone: "UTC",
1450+},
1451+},
1452+},
1453+},
1454+},
1455+},
1456+pluginConfig: {},
1457+ logger,
1458+runtime: {},
1459+on: onMock,
1460+};
1461+1462+try {
1463+registerShortTermPromotionDreamingForTest(api);
1464+let cronAvailable = false;
1465+await triggerGatewayStart(onMock, {
1466+config: api.config,
1467+getCron: () => (cronAvailable ? harness.cron : undefined),
1468+});
1469+1470+api.config = {
1471+plugins: {
1472+entries: {
1473+"memory-core": {
1474+config: {
1475+dreaming: {
1476+enabled: false,
1477+frequency: "15 4 * * *",
1478+timezone: "UTC",
1479+},
1480+},
1481+},
1482+},
1483+},
1484+} as OpenClawConfig;
1485+cronAvailable = true;
1486+1487+await vi.advanceTimersByTimeAsync(constants.STARTUP_CRON_RETRY_DELAY_MS);
1488+await vi.advanceTimersByTimeAsync(constants.STARTUP_CRON_RETRY_DELAY_MS);
1489+1490+expect(logger.error).toHaveBeenCalledWith(
1491+expect.stringContaining("deferred dreaming cron retry failed"),
1492+);
1493+expect(harness.listCalls).toBe(1);
1494+expect(harness.addCalls).toHaveLength(0);
1495+} finally {
1496+vi.useRealTimers();
1497+clearInternalHooks();
1498+}
1499+});
1500+1501+it("clears pending startup cron retry on gateway stop", async () => {
1502+vi.useFakeTimers();
1503+clearInternalHooks();
1504+const logger = createLogger();
1505+const harness = createCronHarness();
1506+const onMock = vi.fn();
1507+const api: DreamingPluginApiTestDouble = {
1508+config: {
1509+plugins: {
1510+entries: {
1511+"memory-core": {
1512+config: {
1513+dreaming: {
1514+enabled: true,
1515+frequency: "15 4 * * *",
1516+timezone: "UTC",
1517+},
1518+},
1519+},
1520+},
1521+},
1522+},
1523+pluginConfig: {},
1524+ logger,
1525+runtime: {},
1526+on: onMock,
1527+};
1528+1529+try {
1530+registerShortTermPromotionDreamingForTest(api);
1531+let cronAvailable = false;
1532+await triggerGatewayStart(onMock, {
1533+config: api.config,
1534+getCron: () => (cronAvailable ? harness.cron : undefined),
1535+});
1536+1537+await triggerGatewayStop(onMock);
1538+cronAvailable = true;
1539+await vi.advanceTimersByTimeAsync(
1540+constants.STARTUP_CRON_RETRY_DELAY_MS * constants.STARTUP_CRON_RETRY_MAX_ATTEMPTS,
1541+);
1542+1543+expect(harness.addCalls).toHaveLength(0);
1544+} finally {
1545+vi.useRealTimers();
1546+clearInternalHooks();
1547+}
1548+});
1549+13351550it("uses live runtime config for heartbeat dreaming reconciliation", async () => {
13361551clearInternalHooks();
13371552const logger = createLogger();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。