























@@ -44,6 +44,13 @@ function createSendMessageMock() {
4444})) as unknown as typeof runtimeSendMessage;
4545}
464647+const longChildCompletionOutput = [
48+"34/34 tests pass, clean build. Now docker repro:",
49+"Root cause: the requester's announce delivery accepted a prefix-only assistant payload as delivered.",
50+"PR: https://github.com/openclaw/openclaw/pull/12345",
51+"Verification: pnpm test src/agents/subagent-announce-delivery.test.ts passed with the regression enabled.",
52+].join("\n");
53+4754async function deliverSlackThreadAnnouncement(params: {
4855callGateway: typeof runtimeCallGateway;
4956isActive: boolean;
@@ -583,6 +590,216 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
583590expect(sendMessage).not.toHaveBeenCalled();
584591});
585592593+it("uses direct fallback when announce-agent delivery returns only a child-result prefix", async () => {
594+const callGateway = createGatewayMock({
595+result: {
596+payloads: [{ text: "34/34 tests pass, clean build. Now docker repro:" }],
597+},
598+});
599+const sendMessage = createSendMessageMock();
600+const result = await deliverSlackThreadAnnouncement({
601+ callGateway,
602+ sendMessage,
603+sessionId: "requester-session-4",
604+isActive: false,
605+expectsCompletionMessage: true,
606+directIdempotencyKey: "announce-thread-fallback-prefix",
607+internalEvents: [
608+{
609+type: "task_completion",
610+source: "subagent",
611+childSessionKey: "agent:worker:subagent:child",
612+childSessionId: "child-session-id",
613+announceType: "subagent task",
614+taskLabel: "thread completion smoke",
615+status: "ok",
616+statusLabel: "completed successfully",
617+result: longChildCompletionOutput,
618+replyInstruction: "Summarize the result.",
619+},
620+],
621+});
622+623+expect(result).toEqual(
624+expect.objectContaining({
625+delivered: true,
626+path: "direct-thread-fallback",
627+}),
628+);
629+expect(sendMessage).toHaveBeenCalledWith(
630+expect.objectContaining({
631+content: longChildCompletionOutput,
632+idempotencyKey: "announce-thread-fallback-prefix",
633+}),
634+);
635+});
636+637+it("uses direct fallback when announce-agent delivery returns a word-boundary child-result prefix", async () => {
638+const callGateway = createGatewayMock({
639+result: {
640+payloads: [{ text: "34/34 tests pass, clean build. Now docker repro" }],
641+},
642+});
643+const sendMessage = createSendMessageMock();
644+const result = await deliverSlackThreadAnnouncement({
645+ callGateway,
646+ sendMessage,
647+sessionId: "requester-session-4",
648+isActive: false,
649+expectsCompletionMessage: true,
650+directIdempotencyKey: "announce-thread-fallback-word-prefix",
651+internalEvents: [
652+{
653+type: "task_completion",
654+source: "subagent",
655+childSessionKey: "agent:worker:subagent:child",
656+childSessionId: "child-session-id",
657+announceType: "subagent task",
658+taskLabel: "thread completion smoke",
659+status: "ok",
660+statusLabel: "completed successfully",
661+result: longChildCompletionOutput,
662+replyInstruction: "Summarize the result.",
663+},
664+],
665+});
666+667+expect(result).toEqual(
668+expect.objectContaining({
669+delivered: true,
670+path: "direct-thread-fallback",
671+}),
672+);
673+expect(sendMessage).toHaveBeenCalledWith(
674+expect.objectContaining({
675+content: longChildCompletionOutput,
676+idempotencyKey: "announce-thread-fallback-word-prefix",
677+}),
678+);
679+});
680+681+it("uses direct fallback when announce-agent delivery returns a mid-word child-result prefix", async () => {
682+const callGateway = createGatewayMock({
683+result: {
684+payloads: [{ text: "34/34 tests pass, clean build. Now dock" }],
685+},
686+});
687+const sendMessage = createSendMessageMock();
688+const result = await deliverSlackThreadAnnouncement({
689+ callGateway,
690+ sendMessage,
691+sessionId: "requester-session-4",
692+isActive: false,
693+expectsCompletionMessage: true,
694+directIdempotencyKey: "announce-thread-fallback-midword-prefix",
695+internalEvents: [
696+{
697+type: "task_completion",
698+source: "subagent",
699+childSessionKey: "agent:worker:subagent:child",
700+childSessionId: "child-session-id",
701+announceType: "subagent task",
702+taskLabel: "thread completion smoke",
703+status: "ok",
704+statusLabel: "completed successfully",
705+result: longChildCompletionOutput,
706+replyInstruction: "Summarize the result.",
707+},
708+],
709+});
710+711+expect(result).toEqual(
712+expect.objectContaining({
713+delivered: true,
714+path: "direct-thread-fallback",
715+}),
716+);
717+expect(sendMessage).toHaveBeenCalledWith(
718+expect.objectContaining({
719+content: longChildCompletionOutput,
720+idempotencyKey: "announce-thread-fallback-midword-prefix",
721+}),
722+);
723+});
724+725+it("keeps concise requester rewrites primary even when child output is long", async () => {
726+const callGateway = createGatewayMock({
727+result: {
728+payloads: [{ text: "Tests passed and the PR is ready for review." }],
729+},
730+});
731+const sendMessage = createSendMessageMock();
732+const result = await deliverSlackThreadAnnouncement({
733+ callGateway,
734+ sendMessage,
735+sessionId: "requester-session-4",
736+isActive: false,
737+expectsCompletionMessage: true,
738+directIdempotencyKey: "announce-thread-rewrite-primary",
739+internalEvents: [
740+{
741+type: "task_completion",
742+source: "subagent",
743+childSessionKey: "agent:worker:subagent:child",
744+childSessionId: "child-session-id",
745+announceType: "subagent task",
746+taskLabel: "thread completion smoke",
747+status: "ok",
748+statusLabel: "completed successfully",
749+result: longChildCompletionOutput,
750+replyInstruction: "Summarize the result.",
751+},
752+],
753+});
754+755+expect(result).toEqual(
756+expect.objectContaining({
757+delivered: true,
758+path: "direct",
759+}),
760+);
761+expect(sendMessage).not.toHaveBeenCalled();
762+});
763+764+it("keeps copied complete-sentence requester summaries primary", async () => {
765+const callGateway = createGatewayMock({
766+result: {
767+payloads: [{ text: "34/34 tests pass, clean build." }],
768+},
769+});
770+const sendMessage = createSendMessageMock();
771+const result = await deliverSlackThreadAnnouncement({
772+ callGateway,
773+ sendMessage,
774+sessionId: "requester-session-4",
775+isActive: false,
776+expectsCompletionMessage: true,
777+directIdempotencyKey: "announce-thread-copied-summary-primary",
778+internalEvents: [
779+{
780+type: "task_completion",
781+source: "subagent",
782+childSessionKey: "agent:worker:subagent:child",
783+childSessionId: "child-session-id",
784+announceType: "subagent task",
785+taskLabel: "thread completion smoke",
786+status: "ok",
787+statusLabel: "completed successfully",
788+result: longChildCompletionOutput,
789+replyInstruction: "Summarize the result.",
790+},
791+],
792+});
793+794+expect(result).toEqual(
795+expect.objectContaining({
796+delivered: true,
797+path: "direct",
798+}),
799+);
800+expect(sendMessage).not.toHaveBeenCalled();
801+});
802+586803it("uses a direct thread fallback when announce-agent delivery fails", async () => {
587804const callGateway = vi.fn(async () => {
588805throw new Error("UNAVAILABLE: gateway lost final output");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。