mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 00:38:28 +00:00
test(agents): collapse repeated announce direct-send scenarios
This commit is contained in:
@@ -595,65 +595,56 @@ describe("subagent announce formatting", () => {
|
|||||||
expect(directTargets).not.toContain("channel:main-parent-channel");
|
expect(directTargets).not.toContain("channel:main-parent-channel");
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it("uses completion direct-send headers for error and timeout outcomes", async () => {
|
||||||
{
|
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
|
||||||
name: "error",
|
const cases = [
|
||||||
childSessionId: "child-session-direct-error",
|
{
|
||||||
requesterSessionId: "requester-session-error",
|
childSessionId: "child-session-direct-error",
|
||||||
childRunId: "run-direct-completion-error",
|
requesterSessionId: "requester-session-error",
|
||||||
replyText: "boom details",
|
childRunId: "run-direct-completion-error",
|
||||||
outcome: { status: "error", error: "boom" } as const,
|
replyText: "boom details",
|
||||||
expectedHeader: "❌ Subagent main failed this task (session remains active)",
|
outcome: { status: "error", error: "boom" } as const,
|
||||||
excludedHeader: "✅ Subagent main",
|
expectedHeader: "❌ Subagent main failed this task (session remains active)",
|
||||||
spawnMode: "session" as const,
|
excludedHeader: "✅ Subagent main",
|
||||||
},
|
spawnMode: "session" as const,
|
||||||
{
|
},
|
||||||
name: "timeout",
|
{
|
||||||
childSessionId: "child-session-direct-timeout",
|
childSessionId: "child-session-direct-timeout",
|
||||||
requesterSessionId: "requester-session-timeout",
|
requesterSessionId: "requester-session-timeout",
|
||||||
childRunId: "run-direct-completion-timeout",
|
childRunId: "run-direct-completion-timeout",
|
||||||
replyText: "partial output",
|
replyText: "partial output",
|
||||||
outcome: { status: "timeout" } as const,
|
outcome: { status: "timeout" } as const,
|
||||||
expectedHeader: "⏱️ Subagent main timed out",
|
expectedHeader: "⏱️ Subagent main timed out",
|
||||||
excludedHeader: "✅ Subagent main finished",
|
excludedHeader: "✅ Subagent main finished",
|
||||||
spawnMode: undefined,
|
spawnMode: undefined,
|
||||||
},
|
},
|
||||||
])(
|
] as const;
|
||||||
"uses completion direct-send header for $name outcomes",
|
|
||||||
async ({
|
for (const testCase of cases) {
|
||||||
childSessionId,
|
sendSpy.mockClear();
|
||||||
requesterSessionId,
|
|
||||||
childRunId,
|
|
||||||
replyText,
|
|
||||||
outcome,
|
|
||||||
expectedHeader,
|
|
||||||
excludedHeader,
|
|
||||||
spawnMode,
|
|
||||||
}) => {
|
|
||||||
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
|
|
||||||
sessionStore = {
|
sessionStore = {
|
||||||
"agent:main:subagent:test": {
|
"agent:main:subagent:test": {
|
||||||
sessionId: childSessionId,
|
sessionId: testCase.childSessionId,
|
||||||
},
|
},
|
||||||
"agent:main:main": {
|
"agent:main:main": {
|
||||||
sessionId: requesterSessionId,
|
sessionId: testCase.requesterSessionId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
chatHistoryMock.mockResolvedValueOnce({
|
chatHistoryMock.mockResolvedValueOnce({
|
||||||
messages: [{ role: "assistant", content: [{ type: "text", text: replyText }] }],
|
messages: [{ role: "assistant", content: [{ type: "text", text: testCase.replyText }] }],
|
||||||
});
|
});
|
||||||
readLatestAssistantReplyMock.mockResolvedValue("");
|
readLatestAssistantReplyMock.mockResolvedValue("");
|
||||||
|
|
||||||
const didAnnounce = await runSubagentAnnounceFlow({
|
const didAnnounce = await runSubagentAnnounceFlow({
|
||||||
childSessionKey: "agent:main:subagent:test",
|
childSessionKey: "agent:main:subagent:test",
|
||||||
childRunId,
|
childRunId: testCase.childRunId,
|
||||||
requesterSessionKey: "agent:main:main",
|
requesterSessionKey: "agent:main:main",
|
||||||
requesterDisplayKey: "main",
|
requesterDisplayKey: "main",
|
||||||
requesterOrigin: { channel: "discord", to: "channel:12345", accountId: "acct-1" },
|
requesterOrigin: { channel: "discord", to: "channel:12345", accountId: "acct-1" },
|
||||||
...defaultOutcomeAnnounce,
|
...defaultOutcomeAnnounce,
|
||||||
outcome,
|
outcome: testCase.outcome,
|
||||||
expectsCompletionMessage: true,
|
expectsCompletionMessage: true,
|
||||||
...(spawnMode ? { spawnMode } : {}),
|
...(testCase.spawnMode ? { spawnMode: testCase.spawnMode } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(didAnnounce).toBe(true);
|
expect(didAnnounce).toBe(true);
|
||||||
@@ -661,163 +652,157 @@ describe("subagent announce formatting", () => {
|
|||||||
const call = sendSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
|
const call = sendSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
|
||||||
const rawMessage = call?.params?.message;
|
const rawMessage = call?.params?.message;
|
||||||
const msg = typeof rawMessage === "string" ? rawMessage : "";
|
const msg = typeof rawMessage === "string" ? rawMessage : "";
|
||||||
expect(msg).toContain(expectedHeader);
|
expect(msg).toContain(testCase.expectedHeader);
|
||||||
expect(msg).toContain(replyText);
|
expect(msg).toContain(testCase.replyText);
|
||||||
expect(msg).not.toContain(excludedHeader);
|
expect(msg).not.toContain(testCase.excludedHeader);
|
||||||
},
|
}
|
||||||
);
|
|
||||||
|
|
||||||
it("ignores stale session thread hints for manual completion direct-send", async () => {
|
|
||||||
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
|
|
||||||
sessionStore = {
|
|
||||||
"agent:main:subagent:test": {
|
|
||||||
sessionId: "child-session-direct-thread",
|
|
||||||
},
|
|
||||||
"agent:main:main": {
|
|
||||||
sessionId: "requester-session-thread",
|
|
||||||
lastChannel: "discord",
|
|
||||||
lastTo: "channel:stale",
|
|
||||||
lastThreadId: 42,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
chatHistoryMock.mockResolvedValueOnce({
|
|
||||||
messages: [{ role: "assistant", content: [{ type: "text", text: "done" }] }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const didAnnounce = await runSubagentAnnounceFlow({
|
|
||||||
childSessionKey: "agent:main:subagent:test",
|
|
||||||
childRunId: "run-direct-stale-thread",
|
|
||||||
requesterSessionKey: "agent:main:main",
|
|
||||||
requesterDisplayKey: "main",
|
|
||||||
requesterOrigin: { channel: "discord", to: "channel:12345", accountId: "acct-1" },
|
|
||||||
...defaultOutcomeAnnounce,
|
|
||||||
expectsCompletionMessage: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(didAnnounce).toBe(true);
|
|
||||||
expect(sendSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(agentSpy).not.toHaveBeenCalled();
|
|
||||||
const call = sendSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
|
|
||||||
expect(call?.params?.channel).toBe("discord");
|
|
||||||
expect(call?.params?.to).toBe("channel:12345");
|
|
||||||
expect(call?.params?.threadId).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes requesterOrigin.threadId for manual completion direct-send", async () => {
|
it("routes manual completion direct-send using requester thread hints", async () => {
|
||||||
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
|
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
|
||||||
sessionStore = {
|
const cases = [
|
||||||
"agent:main:subagent:test": {
|
|
||||||
sessionId: "child-session-direct-thread-pass",
|
|
||||||
},
|
|
||||||
"agent:main:main": {
|
|
||||||
sessionId: "requester-session-thread-pass",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
chatHistoryMock.mockResolvedValueOnce({
|
|
||||||
messages: [{ role: "assistant", content: [{ type: "text", text: "done" }] }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const didAnnounce = await runSubagentAnnounceFlow({
|
|
||||||
childSessionKey: "agent:main:subagent:test",
|
|
||||||
childRunId: "run-direct-thread-pass",
|
|
||||||
requesterSessionKey: "agent:main:main",
|
|
||||||
requesterDisplayKey: "main",
|
|
||||||
requesterOrigin: {
|
|
||||||
channel: "discord",
|
|
||||||
to: "channel:12345",
|
|
||||||
accountId: "acct-1",
|
|
||||||
threadId: 99,
|
|
||||||
},
|
|
||||||
...defaultOutcomeAnnounce,
|
|
||||||
expectsCompletionMessage: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(didAnnounce).toBe(true);
|
|
||||||
expect(sendSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(agentSpy).not.toHaveBeenCalled();
|
|
||||||
const call = sendSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
|
|
||||||
expect(call?.params?.channel).toBe("discord");
|
|
||||||
expect(call?.params?.to).toBe("channel:12345");
|
|
||||||
expect(call?.params?.threadId).toBe("99");
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
{
|
|
||||||
name: "requester threadId matches hook target",
|
|
||||||
childRunId: "run-direct-thread-bound",
|
|
||||||
requesterOrigin: {
|
|
||||||
channel: "discord",
|
|
||||||
to: "channel:12345",
|
|
||||||
accountId: "acct-1",
|
|
||||||
threadId: "777",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "requester origin has no threadId",
|
|
||||||
childRunId: "run-direct-thread-bound-single",
|
|
||||||
requesterOrigin: {
|
|
||||||
channel: "discord",
|
|
||||||
to: "channel:12345",
|
|
||||||
accountId: "acct-1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "requester threadId does not match",
|
|
||||||
childRunId: "run-direct-thread-no-match",
|
|
||||||
requesterOrigin: {
|
|
||||||
channel: "discord",
|
|
||||||
to: "channel:12345",
|
|
||||||
accountId: "acct-1",
|
|
||||||
threadId: "999",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])("uses hook-provided thread target when $name", async ({ childRunId, requesterOrigin }) => {
|
|
||||||
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
|
|
||||||
hasSubagentDeliveryTargetHook = true;
|
|
||||||
subagentDeliveryTargetHookMock.mockResolvedValueOnce({
|
|
||||||
origin: {
|
|
||||||
channel: "discord",
|
|
||||||
accountId: "acct-1",
|
|
||||||
to: "channel:777",
|
|
||||||
threadId: "777",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const didAnnounce = await runSubagentAnnounceFlow({
|
|
||||||
childSessionKey: "agent:main:subagent:test",
|
|
||||||
childRunId,
|
|
||||||
requesterSessionKey: "agent:main:main",
|
|
||||||
requesterDisplayKey: "main",
|
|
||||||
requesterOrigin,
|
|
||||||
...defaultOutcomeAnnounce,
|
|
||||||
expectsCompletionMessage: true,
|
|
||||||
spawnMode: "session",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(didAnnounce).toBe(true);
|
|
||||||
expect(subagentDeliveryTargetHookMock).toHaveBeenCalledWith(
|
|
||||||
{
|
{
|
||||||
|
childSessionId: "child-session-direct-thread",
|
||||||
|
requesterSessionId: "requester-session-thread",
|
||||||
|
childRunId: "run-direct-stale-thread",
|
||||||
|
requesterOrigin: { channel: "discord", to: "channel:12345", accountId: "acct-1" },
|
||||||
|
requesterSessionMeta: {
|
||||||
|
lastChannel: "discord",
|
||||||
|
lastTo: "channel:stale",
|
||||||
|
lastThreadId: 42,
|
||||||
|
},
|
||||||
|
expectedThreadId: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
childSessionId: "child-session-direct-thread-pass",
|
||||||
|
requesterSessionId: "requester-session-thread-pass",
|
||||||
|
childRunId: "run-direct-thread-pass",
|
||||||
|
requesterOrigin: {
|
||||||
|
channel: "discord",
|
||||||
|
to: "channel:12345",
|
||||||
|
accountId: "acct-1",
|
||||||
|
threadId: 99,
|
||||||
|
},
|
||||||
|
requesterSessionMeta: {},
|
||||||
|
expectedThreadId: "99",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const testCase of cases) {
|
||||||
|
sendSpy.mockClear();
|
||||||
|
agentSpy.mockClear();
|
||||||
|
sessionStore = {
|
||||||
|
"agent:main:subagent:test": {
|
||||||
|
sessionId: testCase.childSessionId,
|
||||||
|
},
|
||||||
|
"agent:main:main": {
|
||||||
|
sessionId: testCase.requesterSessionId,
|
||||||
|
...testCase.requesterSessionMeta,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
chatHistoryMock.mockResolvedValueOnce({
|
||||||
|
messages: [{ role: "assistant", content: [{ type: "text", text: "done" }] }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const didAnnounce = await runSubagentAnnounceFlow({
|
||||||
childSessionKey: "agent:main:subagent:test",
|
childSessionKey: "agent:main:subagent:test",
|
||||||
|
childRunId: testCase.childRunId,
|
||||||
requesterSessionKey: "agent:main:main",
|
requesterSessionKey: "agent:main:main",
|
||||||
requesterOrigin,
|
requesterDisplayKey: "main",
|
||||||
childRunId,
|
requesterOrigin: testCase.requesterOrigin,
|
||||||
spawnMode: "session",
|
...defaultOutcomeAnnounce,
|
||||||
expectsCompletionMessage: true,
|
expectsCompletionMessage: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(didAnnounce).toBe(true);
|
||||||
|
expect(sendSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(agentSpy).not.toHaveBeenCalled();
|
||||||
|
const call = sendSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
|
||||||
|
expect(call?.params?.channel).toBe("discord");
|
||||||
|
expect(call?.params?.to).toBe("channel:12345");
|
||||||
|
expect(call?.params?.threadId).toBe(testCase.expectedThreadId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses hook-provided thread target across requester thread variants", async () => {
|
||||||
|
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
childRunId: "run-direct-thread-bound",
|
||||||
|
requesterOrigin: {
|
||||||
|
channel: "discord",
|
||||||
|
to: "channel:12345",
|
||||||
|
accountId: "acct-1",
|
||||||
|
threadId: "777",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
runId: childRunId,
|
childRunId: "run-direct-thread-bound-single",
|
||||||
childSessionKey: "agent:main:subagent:test",
|
requesterOrigin: {
|
||||||
requesterSessionKey: "agent:main:main",
|
channel: "discord",
|
||||||
|
to: "channel:12345",
|
||||||
|
accountId: "acct-1",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
{
|
||||||
expect(sendSpy).toHaveBeenCalledTimes(1);
|
childRunId: "run-direct-thread-no-match",
|
||||||
const call = sendSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
|
requesterOrigin: {
|
||||||
expect(call?.params?.channel).toBe("discord");
|
channel: "discord",
|
||||||
expect(call?.params?.to).toBe("channel:777");
|
to: "channel:12345",
|
||||||
expect(call?.params?.threadId).toBe("777");
|
accountId: "acct-1",
|
||||||
const message = typeof call?.params?.message === "string" ? call.params.message : "";
|
threadId: "999",
|
||||||
expect(message).toContain("completed this task (session remains active)");
|
},
|
||||||
expect(message).not.toContain("finished");
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const testCase of cases) {
|
||||||
|
sendSpy.mockClear();
|
||||||
|
hasSubagentDeliveryTargetHook = true;
|
||||||
|
subagentDeliveryTargetHookMock.mockResolvedValueOnce({
|
||||||
|
origin: {
|
||||||
|
channel: "discord",
|
||||||
|
accountId: "acct-1",
|
||||||
|
to: "channel:777",
|
||||||
|
threadId: "777",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const didAnnounce = await runSubagentAnnounceFlow({
|
||||||
|
childSessionKey: "agent:main:subagent:test",
|
||||||
|
childRunId: testCase.childRunId,
|
||||||
|
requesterSessionKey: "agent:main:main",
|
||||||
|
requesterDisplayKey: "main",
|
||||||
|
requesterOrigin: testCase.requesterOrigin,
|
||||||
|
...defaultOutcomeAnnounce,
|
||||||
|
expectsCompletionMessage: true,
|
||||||
|
spawnMode: "session",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(didAnnounce).toBe(true);
|
||||||
|
expect(subagentDeliveryTargetHookMock).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
childSessionKey: "agent:main:subagent:test",
|
||||||
|
requesterSessionKey: "agent:main:main",
|
||||||
|
requesterOrigin: testCase.requesterOrigin,
|
||||||
|
childRunId: testCase.childRunId,
|
||||||
|
spawnMode: "session",
|
||||||
|
expectsCompletionMessage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
runId: testCase.childRunId,
|
||||||
|
childSessionKey: "agent:main:subagent:test",
|
||||||
|
requesterSessionKey: "agent:main:main",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(sendSpy).toHaveBeenCalledTimes(1);
|
||||||
|
const call = sendSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
|
||||||
|
expect(call?.params?.channel).toBe("discord");
|
||||||
|
expect(call?.params?.to).toBe("channel:777");
|
||||||
|
expect(call?.params?.threadId).toBe("777");
|
||||||
|
const message = typeof call?.params?.message === "string" ? call.params.message : "";
|
||||||
|
expect(message).toContain("completed this task (session remains active)");
|
||||||
|
expect(message).not.toContain("finished");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
|
|||||||
Reference in New Issue
Block a user