mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
test(agents): consolidate run wait reply scenarios (#118469)
This commit is contained in:
committed by
GitHub
parent
e163c3c621
commit
2bb2943e93
+184
-424
@@ -460,464 +460,224 @@ describe("waitForAgentRunAndReadUpdatedAssistantReply", () => {
|
||||
callGatewayMock.mockReset();
|
||||
});
|
||||
|
||||
it("returns undefined when the latest assistant fingerprint matches the baseline", async () => {
|
||||
const assistantMessage = {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "same reply" }],
|
||||
timestamp: 42,
|
||||
};
|
||||
callGatewayMock
|
||||
.mockResolvedValueOnce({
|
||||
status: "ok",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
messages: [assistantMessage],
|
||||
});
|
||||
type TranscriptMessage = Record<string, unknown>;
|
||||
type WaitedReplyCase = {
|
||||
name: string;
|
||||
runId: string;
|
||||
messages: TranscriptMessage[];
|
||||
expected: Record<string, unknown>;
|
||||
baseline?: { text?: string; fingerprint?: string };
|
||||
sessionKey?: string;
|
||||
wait?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const result = await waitForAgentRunAndReadUpdatedAssistantReply({
|
||||
const assistant = (text: string, metadata: TranscriptMessage = {}): TranscriptMessage => ({
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text }],
|
||||
...metadata,
|
||||
});
|
||||
const interSession = (text: string, metadata: TranscriptMessage = {}): TranscriptMessage =>
|
||||
assistant(text, {
|
||||
provenance: {
|
||||
kind: "inter_session",
|
||||
sourceSessionKey: "agent:main:source",
|
||||
sourceTool: "sessions_send",
|
||||
},
|
||||
...metadata,
|
||||
});
|
||||
const messageToolMirror = (
|
||||
text: string,
|
||||
mirror: TranscriptMessage,
|
||||
metadata: TranscriptMessage = {},
|
||||
): TranscriptMessage =>
|
||||
assistant(text, {
|
||||
openclawMessageToolMirror: { toolName: "message", ...mirror },
|
||||
...metadata,
|
||||
});
|
||||
|
||||
const sameReply = assistant("same reply", { timestamp: 42 });
|
||||
const previousReply = assistant("previous real reply", { timestamp: 41 });
|
||||
const forwardedRequest = interSession("forwarded request", {
|
||||
__openclaw: { seq: 41 },
|
||||
timestamp: 41,
|
||||
});
|
||||
const pendingSourceReply = messageToolMirror(
|
||||
"source reply awaiting delivery",
|
||||
{
|
||||
toolCallId: "call-message-send",
|
||||
sourceReplySink: "internal-ui",
|
||||
sourceMessageSeq: 42,
|
||||
},
|
||||
{ timestamp: 42 },
|
||||
);
|
||||
const olderBaseline = { text: "older reply", fingerprint: "old-fingerprint" };
|
||||
|
||||
const cases: WaitedReplyCase[] = [
|
||||
{
|
||||
name: "returns undefined when the latest assistant fingerprint matches the baseline",
|
||||
runId: "run-1",
|
||||
sessionKey: "agent:main:child",
|
||||
timeoutMs: 1_000,
|
||||
baseline: {
|
||||
text: "same reply",
|
||||
fingerprint: JSON.stringify(assistantMessage),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
status: "ok",
|
||||
replyText: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns undefined when a text-only baseline matches the latest assistant reply", async () => {
|
||||
callGatewayMock
|
||||
.mockResolvedValueOnce({
|
||||
status: "ok",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "same reply" }],
|
||||
timestamp: 42,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await waitForAgentRunAndReadUpdatedAssistantReply({
|
||||
messages: [sameReply],
|
||||
baseline: { text: "same reply", fingerprint: JSON.stringify(sameReply) },
|
||||
expected: { status: "ok", replyText: undefined },
|
||||
},
|
||||
{
|
||||
name: "returns undefined when a text-only baseline matches the latest assistant reply",
|
||||
runId: "run-text-baseline",
|
||||
sessionKey: "agent:main:child",
|
||||
timeoutMs: 1_000,
|
||||
baseline: {
|
||||
text: "same reply",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
status: "ok",
|
||||
replyText: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not treat a message-tool delivery mirror as a new waited reply", async () => {
|
||||
const baselineMessage = {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "previous real reply" }],
|
||||
timestamp: 41,
|
||||
};
|
||||
callGatewayMock
|
||||
.mockResolvedValueOnce({
|
||||
status: "ok",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
messages: [
|
||||
baselineMessage,
|
||||
{
|
||||
role: "assistant",
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
content: [{ type: "text", text: "already delivered source reply" }],
|
||||
timestamp: 42,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await waitForAgentRunAndReadUpdatedAssistantReply({
|
||||
messages: [sameReply],
|
||||
baseline: { text: "same reply" },
|
||||
expected: { status: "ok", replyText: undefined },
|
||||
},
|
||||
{
|
||||
name: "does not treat a message-tool delivery mirror as a new waited reply",
|
||||
runId: "run-source-reply",
|
||||
sessionKey: "agent:main:child",
|
||||
timeoutMs: 1_000,
|
||||
baseline: {
|
||||
text: "previous real reply",
|
||||
fingerprint: JSON.stringify(baselineMessage),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
status: "ok",
|
||||
replyText: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not treat a projected message-tool mirror as a new waited reply", async () => {
|
||||
const baselineMessage = {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "previous real reply" }],
|
||||
timestamp: 41,
|
||||
};
|
||||
callGatewayMock
|
||||
.mockResolvedValueOnce({
|
||||
status: "ok",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
messages: [
|
||||
baselineMessage,
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "already delivered source reply" }],
|
||||
openclawMessageToolMirror: {
|
||||
toolName: "message",
|
||||
toolCallId: "call-message-send",
|
||||
},
|
||||
timestamp: 42,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await waitForAgentRunAndReadUpdatedAssistantReply({
|
||||
runId: "run-projected-source-reply",
|
||||
sessionKey: "agent:main:child",
|
||||
timeoutMs: 1_000,
|
||||
baseline: {
|
||||
text: "previous real reply",
|
||||
fingerprint: JSON.stringify(baselineMessage),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
status: "ok",
|
||||
replyText: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a projected message-tool reply held for outer A2A delivery", async () => {
|
||||
callGatewayMock.mockResolvedValueOnce({ status: "ok" }).mockResolvedValueOnce({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
provenance: {
|
||||
kind: "inter_session",
|
||||
sourceSessionKey: "agent:main:source",
|
||||
sourceTool: "sessions_send",
|
||||
},
|
||||
content: [{ type: "text", text: "forwarded request" }],
|
||||
__openclaw: { seq: 41 },
|
||||
timestamp: 41,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "source reply awaiting delivery" }],
|
||||
openclawMessageToolMirror: {
|
||||
toolName: "message",
|
||||
toolCallId: "call-message-send",
|
||||
sourceReplySink: "internal-ui",
|
||||
sourceMessageSeq: 42,
|
||||
},
|
||||
previousReply,
|
||||
assistant("already delivered source reply", {
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
timestamp: 42,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const result = await waitForAgentRunAndReadUpdatedAssistantReply({
|
||||
baseline: { text: "previous real reply", fingerprint: JSON.stringify(previousReply) },
|
||||
expected: { status: "ok", replyText: undefined },
|
||||
},
|
||||
{
|
||||
name: "does not treat a projected message-tool mirror as a new waited reply",
|
||||
runId: "run-projected-source-reply",
|
||||
messages: [
|
||||
previousReply,
|
||||
messageToolMirror(
|
||||
"already delivered source reply",
|
||||
{ toolCallId: "call-message-send" },
|
||||
{ timestamp: 42 },
|
||||
),
|
||||
],
|
||||
baseline: { text: "previous real reply", fingerprint: JSON.stringify(previousReply) },
|
||||
expected: { status: "ok", replyText: undefined },
|
||||
},
|
||||
{
|
||||
name: "returns a projected message-tool reply held for outer A2A delivery",
|
||||
runId: "run-internal-source-reply",
|
||||
sessionKey: "agent:worker:main",
|
||||
timeoutMs: 1_000,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
status: "ok",
|
||||
replyText: "source reply awaiting delivery",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers an internal source reply over a later private final", async () => {
|
||||
callGatewayMock.mockResolvedValueOnce({ status: "ok" }).mockResolvedValueOnce({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
provenance: {
|
||||
kind: "inter_session",
|
||||
sourceSessionKey: "agent:main:source",
|
||||
sourceTool: "sessions_send",
|
||||
},
|
||||
content: [{ type: "text", text: "forwarded request" }],
|
||||
__openclaw: { seq: 41 },
|
||||
timestamp: 41,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "source reply awaiting delivery" }],
|
||||
openclawMessageToolMirror: {
|
||||
toolName: "message",
|
||||
toolCallId: "call-message-send",
|
||||
sourceReplySink: "internal-ui",
|
||||
sourceMessageSeq: 42,
|
||||
},
|
||||
timestamp: 42,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Done" }],
|
||||
timestamp: 43,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await waitForAgentRunAndReadUpdatedAssistantReply({
|
||||
messages: [forwardedRequest, pendingSourceReply],
|
||||
expected: { status: "ok", replyText: "source reply awaiting delivery" },
|
||||
},
|
||||
{
|
||||
name: "prefers an internal source reply over a later private final",
|
||||
runId: "run-internal-source-reply-with-private-final",
|
||||
sessionKey: "agent:worker:main",
|
||||
timeoutMs: 1_000,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
status: "ok",
|
||||
replyText: "source reply awaiting delivery",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not let a late internal result cross an inter-session turn boundary", async () => {
|
||||
callGatewayMock.mockResolvedValueOnce({ status: "ok" }).mockResolvedValueOnce({
|
||||
messages: [forwardedRequest, pendingSourceReply, assistant("Done", { timestamp: 43 })],
|
||||
expected: { status: "ok", replyText: "source reply awaiting delivery" },
|
||||
},
|
||||
{
|
||||
name: "does not let a late internal result cross an inter-session turn boundary",
|
||||
runId: "run-after-late-internal-source-reply",
|
||||
sessionKey: "agent:worker:main",
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
provenance: {
|
||||
kind: "inter_session",
|
||||
sourceSessionKey: "agent:main:source",
|
||||
sourceTool: "sessions_send",
|
||||
},
|
||||
content: [{ type: "text", text: "new forwarded request" }],
|
||||
__openclaw: { seq: 42 },
|
||||
timestamp: 42,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "stale source reply" }],
|
||||
openclawMessageToolMirror: {
|
||||
toolName: "message",
|
||||
interSession("new forwarded request", { __openclaw: { seq: 42 }, timestamp: 42 }),
|
||||
messageToolMirror(
|
||||
"stale source reply",
|
||||
{
|
||||
toolCallId: "call-message-before-request",
|
||||
sourceReplySink: "internal-ui",
|
||||
sourceMessageSeq: 41,
|
||||
},
|
||||
timestamp: 41,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "fresh reply" }],
|
||||
timestamp: 43,
|
||||
},
|
||||
{ timestamp: 41 },
|
||||
),
|
||||
assistant("fresh reply", { timestamp: 43 }),
|
||||
],
|
||||
});
|
||||
|
||||
const result = await waitForAgentRunAndReadUpdatedAssistantReply({
|
||||
runId: "run-after-late-internal-source-reply",
|
||||
sessionKey: "agent:worker:main",
|
||||
timeoutMs: 1_000,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
status: "ok",
|
||||
replyText: "fresh reply",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not return a private final written after a message-tool delivery mirror", async () => {
|
||||
callGatewayMock.mockResolvedValueOnce({ status: "ok" }).mockResolvedValueOnce({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
provenance: {
|
||||
kind: "inter_session",
|
||||
sourceSessionKey: "agent:main:source",
|
||||
sourceTool: "sessions_send",
|
||||
},
|
||||
content: [{ type: "text", text: "forwarded request" }],
|
||||
timestamp: 41,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "already delivered source reply" }],
|
||||
openclawMessageToolMirror: {
|
||||
toolName: "message",
|
||||
toolCallId: "call-message-send",
|
||||
},
|
||||
timestamp: 42,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Done" }],
|
||||
timestamp: 43,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await waitForAgentRunAndReadUpdatedAssistantReply({
|
||||
expected: { status: "ok", replyText: "fresh reply" },
|
||||
},
|
||||
{
|
||||
name: "does not return a private final written after a message-tool delivery mirror",
|
||||
runId: "run-source-reply-with-private-final",
|
||||
sessionKey: "agent:main:child",
|
||||
timeoutMs: 1_000,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
status: "ok",
|
||||
replyText: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not let an older turn's message-tool mirror suppress a fresh reply", async () => {
|
||||
callGatewayMock.mockResolvedValueOnce({ status: "ok" }).mockResolvedValueOnce({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "older delivered reply" }],
|
||||
openclawMessageToolMirror: {
|
||||
toolName: "message",
|
||||
toolCallId: "call-older-message-send",
|
||||
},
|
||||
timestamp: 40,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
provenance: {
|
||||
kind: "inter_session",
|
||||
sourceSessionKey: "agent:main:source",
|
||||
sourceTool: "sessions_send",
|
||||
},
|
||||
content: [{ type: "text", text: "new forwarded request" }],
|
||||
timestamp: 41,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "fresh reply" }],
|
||||
timestamp: 42,
|
||||
},
|
||||
interSession("forwarded request", { timestamp: 41 }),
|
||||
messageToolMirror(
|
||||
"already delivered source reply",
|
||||
{ toolCallId: "call-message-send" },
|
||||
{ timestamp: 42 },
|
||||
),
|
||||
assistant("Done", { timestamp: 43 }),
|
||||
],
|
||||
});
|
||||
|
||||
const result = await waitForAgentRunAndReadUpdatedAssistantReply({
|
||||
expected: { status: "ok", replyText: undefined },
|
||||
},
|
||||
{
|
||||
name: "does not let an older turn's message-tool mirror suppress a fresh reply",
|
||||
runId: "run-after-older-source-reply",
|
||||
sessionKey: "agent:main:child",
|
||||
timeoutMs: 1_000,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
status: "ok",
|
||||
replyText: "fresh reply",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not resurrect an older reply when only a delivery mirror is newer", async () => {
|
||||
callGatewayMock
|
||||
.mockResolvedValueOnce({
|
||||
status: "ok",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "stale previous reply" }],
|
||||
timestamp: 41,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
content: [{ type: "text", text: "already delivered source reply" }],
|
||||
timestamp: 42,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await waitForAgentRunAndReadUpdatedAssistantReply({
|
||||
messages: [
|
||||
messageToolMirror(
|
||||
"older delivered reply",
|
||||
{ toolCallId: "call-older-message-send" },
|
||||
{ timestamp: 40 },
|
||||
),
|
||||
interSession("new forwarded request", { timestamp: 41 }),
|
||||
assistant("fresh reply", { timestamp: 42 }),
|
||||
],
|
||||
expected: { status: "ok", replyText: "fresh reply" },
|
||||
},
|
||||
{
|
||||
name: "does not resurrect an older reply when only a delivery mirror is newer",
|
||||
runId: "run-source-reply-without-baseline",
|
||||
sessionKey: "agent:main:child",
|
||||
timeoutMs: 1_000,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
status: "ok",
|
||||
replyText: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the new assistant text when the fingerprint changes", async () => {
|
||||
callGatewayMock
|
||||
.mockResolvedValueOnce({
|
||||
status: "ok",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "fresh reply" }],
|
||||
timestamp: 99,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await waitForAgentRunAndReadUpdatedAssistantReply({
|
||||
messages: [
|
||||
assistant("stale previous reply", { timestamp: 41 }),
|
||||
assistant("already delivered source reply", {
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
timestamp: 42,
|
||||
}),
|
||||
],
|
||||
expected: { status: "ok", replyText: undefined },
|
||||
},
|
||||
{
|
||||
name: "returns the new assistant text when the fingerprint changes",
|
||||
runId: "run-2",
|
||||
sessionKey: "agent:main:child",
|
||||
timeoutMs: 1_000,
|
||||
baseline: {
|
||||
text: "older reply",
|
||||
fingerprint: "old-fingerprint",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
status: "ok",
|
||||
replyText: "fresh reply",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves successful wait metadata when returning an updated reply", async () => {
|
||||
callGatewayMock
|
||||
.mockResolvedValueOnce({
|
||||
messages: [assistant("fresh reply", { timestamp: 99 })],
|
||||
baseline: olderBaseline,
|
||||
expected: { status: "ok", replyText: "fresh reply" },
|
||||
},
|
||||
{
|
||||
name: "preserves successful wait metadata when returning an updated reply",
|
||||
runId: "run-with-metadata",
|
||||
messages: [assistant("fresh reply", { timestamp: 99 })],
|
||||
baseline: olderBaseline,
|
||||
wait: {
|
||||
status: "ok",
|
||||
startedAt: 100,
|
||||
endedAt: 200,
|
||||
stopReason: "completed",
|
||||
yielded: true,
|
||||
providerStarted: true,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "fresh reply" }],
|
||||
timestamp: 99,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
expected: {
|
||||
status: "ok",
|
||||
startedAt: 100,
|
||||
endedAt: 200,
|
||||
stopReason: "completed",
|
||||
yielded: true,
|
||||
providerStarted: true,
|
||||
replyText: "fresh reply",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(cases)("$name", async ({ runId, messages, expected, baseline, sessionKey, wait }) => {
|
||||
callGatewayMock
|
||||
.mockResolvedValueOnce(wait ?? { status: "ok" })
|
||||
.mockResolvedValueOnce({ messages });
|
||||
|
||||
const result = await waitForAgentRunAndReadUpdatedAssistantReply({
|
||||
runId: "run-with-metadata",
|
||||
sessionKey: "agent:main:child",
|
||||
runId,
|
||||
sessionKey: sessionKey ?? "agent:main:child",
|
||||
timeoutMs: 1_000,
|
||||
baseline: {
|
||||
text: "older reply",
|
||||
fingerprint: "old-fingerprint",
|
||||
},
|
||||
baseline,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
status: "ok",
|
||||
startedAt: 100,
|
||||
endedAt: 200,
|
||||
stopReason: "completed",
|
||||
yielded: true,
|
||||
providerStarted: true,
|
||||
replyText: "fresh reply",
|
||||
});
|
||||
expect(result).toEqual(expected);
|
||||
expect(callGatewayMock.mock.calls.map(([request]) => request.method)).toEqual([
|
||||
"agent.wait",
|
||||
"chat.history",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user