





















11// Subagent announce delivery tests cover the last-mile routing used when child
22// runs report progress or completion back to the requester session.
3-import fs from "node:fs/promises";
4-import os from "node:os";
5-import path from "node:path";
63import { afterEach, describe, expect, it, vi } from "vitest";
74import { OutboundDeliveryError } from "../infra/outbound/deliver-types.js";
85import {
@@ -4852,183 +4849,3 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
48524849});
48534850});
48544851});
4855-4856-describe("deliverSubagentAnnouncement requester session backfill (issue #86034)", () => {
4857-// Regression: image_generate launched from a non-direct-reply turn (heartbeat,
4858-// cron, subagent spawn) supplies a completion origin missing `to`. The
4859-// already-loaded requester session entry carries `lastTo`/`lastChannel`, so
4860-// effectiveDirectOrigin must backfill from it before the deliverability gate,
4861-// otherwise generated media is silently dropped.
4862-it("backfills to/accountId from the requester session entry when completion origin is incomplete", async () => {
4863-const agentId = `backfill-${Date.now()}-${Math.random().toString(16).slice(2)}`;
4864-const sessionKey = `agent:${agentId}:telegram:5866004662`;
4865-const storeTemplate = path.join(
4866-os.tmpdir(),
4867-`openclaw-86034-session-${agentId}-{agentId}.json`,
4868-);
4869-const storePath = storeTemplate.replaceAll("{agentId}", agentId);
4870-await fs.writeFile(
4871-storePath,
4872-JSON.stringify(
4873-{
4874-[sessionKey]: {
4875-sessionId: "telegram-session-1",
4876-updatedAt: Date.now(),
4877-channel: "telegram",
4878-lastChannel: "telegram",
4879-lastTo: "5866004662",
4880-lastAccountId: "bot-1",
4881-},
4882-},
4883-null,
4884-2,
4885-),
4886-"utf-8",
4887-);
4888-4889-try {
4890-const dispatchGatewayMethodInProcess = createInProcessGatewayMock({
4891-result: { payloads: [{ text: "requester voice completion" }] },
4892-});
4893-testing.setDepsForTest({
4894- dispatchGatewayMethodInProcess,
4895-getRequesterSessionActivity: () => ({
4896-sessionId: "telegram-session-1",
4897-isActive: false,
4898-}),
4899-getRuntimeConfig: () => ({ session: { store: storeTemplate } }) as never,
4900-});
4901-4902-const result = await deliverSubagentAnnouncement({
4903-requesterSessionKey: sessionKey,
4904-targetRequesterSessionKey: sessionKey,
4905-triggerMessage: "image done",
4906-steerMessage: "image done",
4907-// Origin carries channel but NOT `to` or `accountId` — simulates an
4908-// image_generate task created off the direct-reply path.
4909-requesterOrigin: { channel: "telegram" },
4910-requesterSessionOrigin: { channel: "telegram" },
4911-completionDirectOrigin: { channel: "telegram" },
4912-directOrigin: { channel: "telegram" },
4913-requesterIsSubagent: false,
4914-expectsCompletionMessage: true,
4915-bestEffortDeliver: true,
4916-directIdempotencyKey: "announce-86034-backfill",
4917-sourceTool: "image_generate",
4918-});
4919-4920-expectRecordFields(result, {
4921-delivered: true,
4922-path: "direct",
4923-});
4924-// The deliverability decision must see the backfilled `to`.
4925-expectInProcessAgentParams(dispatchGatewayMethodInProcess, {
4926-deliver: true,
4927-channel: "telegram",
4928-accountId: "bot-1",
4929-to: "5866004662",
4930-});
4931-} finally {
4932-await fs.rm(storePath, { force: true });
4933-}
4934-});
4935-4936-// Cross-channel safety regression: a stale lastChannel that differs from
4937-// the completion origin's channel must not leak its lastTo into the
4938-// telegram delivery. mergeDeliveryContext.channelsConflict (delivery-context.shared.ts:233-260)
4939-// is the structural guard; this test locks it.
4940-it("does not import a cross-channel lastTo when the completion origin's channel differs from lastChannel (cross-channel guard regression)", async () => {
4941-const agentId = `xchan-${Date.now()}-${Math.random().toString(16).slice(2)}`;
4942-const sessionKey = `agent:${agentId}:telegram:5866004662`;
4943-const storeTemplate = path.join(
4944-os.tmpdir(),
4945-`openclaw-86034-xchan-session-${agentId}-{agentId}.json`,
4946-);
4947-const storePath = storeTemplate.replaceAll("{agentId}", agentId);
4948-await fs.writeFile(
4949-storePath,
4950-JSON.stringify(
4951-{
4952-[sessionKey]: {
4953-sessionId: "telegram-session-xchan",
4954-updatedAt: Date.now(),
4955-channel: "telegram",
4956-lastChannel: "signal",
4957-lastTo: "signal-stale-target",
4958-lastAccountId: "signal-bot-1",
4959-},
4960-},
4961-null,
4962-2,
4963-),
4964-"utf-8",
4965-);
4966-4967-try {
4968-const dispatchGatewayMethodInProcess = createInProcessGatewayMock({
4969-result: { payloads: [{ text: "requester voice completion" }] },
4970-});
4971-testing.setDepsForTest({
4972- dispatchGatewayMethodInProcess,
4973-getRequesterSessionActivity: () => ({
4974-sessionId: "telegram-session-xchan",
4975-isActive: false,
4976-}),
4977-getRuntimeConfig: () => ({ session: { store: storeTemplate } }) as never,
4978-});
4979-4980-const result = await deliverSubagentAnnouncement({
4981-requesterSessionKey: sessionKey,
4982-targetRequesterSessionKey: sessionKey,
4983-triggerMessage: "image done",
4984-steerMessage: "image done",
4985-requesterOrigin: { channel: "telegram", accountId: "telegram-bot-1" },
4986-requesterSessionOrigin: { channel: "telegram", accountId: "telegram-bot-1" },
4987-completionDirectOrigin: { channel: "telegram", accountId: "telegram-bot-1" },
4988-directOrigin: { channel: "telegram", accountId: "telegram-bot-1" },
4989-requesterIsSubagent: false,
4990-expectsCompletionMessage: true,
4991-bestEffortDeliver: true,
4992-directIdempotencyKey: "announce-86034-cross-channel",
4993-sourceTool: "image_generate",
4994-});
4995-4996-// Structural assertion: the stale signal `to` must not have leaked into
4997-// any in-process gateway dispatch, regardless of whether the call ended
4998-// up routed (path: "direct") or short-circuited (delivered: false /
4999-// path: "none"). The guard is "no cross-channel `to` ever reaches the
5000-// gateway", not a specific terminal path.
5001-expect(dispatchGatewayMethodInProcess).not.toHaveBeenCalledWith(
5002-"agent",
5003-expect.objectContaining({ to: "signal-stale-target" }),
5004-expect.anything(),
5005-);
5006-expect(dispatchGatewayMethodInProcess).not.toHaveBeenCalledWith(
5007-"agent",
5008-expect.objectContaining({ channel: "signal" }),
5009-expect.anything(),
5010-);
5011-for (const call of asMock(dispatchGatewayMethodInProcess).mock.calls) {
5012-const params = call[1] as Record<string, unknown> | undefined;
5013-if (!params) {
5014-continue;
5015-}
5016-expect(params.to).not.toBe("signal-stale-target");
5017-expect(params.channel).not.toBe("signal");
5018-}
5019-5020-// Result-shape assertion: if the dispatcher was invoked at all on the
5021-// direct path, it must not have been with the stale signal target.
5022-if (
5023-(result as { path?: string }).path === "direct" &&
5024-(result as { delivered?: boolean }).delivered === true
5025-) {
5026-const params = mockCallArg(dispatchGatewayMethodInProcess, 0, 1) as Record<string, unknown>;
5027-expect(params.to).not.toBe("signal-stale-target");
5028-expect(params.channel).not.toBe("signal");
5029-}
5030-} finally {
5031-await fs.rm(storePath, { force: true });
5032-}
5033-});
5034-});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。